0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita100万記事感謝祭!記事投稿キャンペーン開催のお知らせ

【ERC-20】Polygon Amoy Testnet にスマートコントラクトをデプロイする

Posted at

はじめに

初投稿です!
普段は太陽光発電の販売会社で経理や情シスをやりつつ、スタートアップでSwiftエンジニアをしているkomeponと申します!
主にNext.jsで社内システムを作ったり、ブロックチェーン・バックエンド・インフラと幅広く触れています。

初記事にしてはどうかと思うのですが、今回は、Polygon Amoy TestnetにERC-20規格準拠のスマートコントラクトを作成・デプロイする方法についてざっくり説明します。

コントラクトについて

このERCは、イーサリアム互換チェーン(今回はPolygon)上で、フラッシュローンを用いて異なる取引所間での価格差を利用したアービトラージを行うときに必要なルールや手順を定めたものとなります。

フラッシュローンとは、あるコントラクトから別のコントラクトへ一時的に資産を貸し出すシステムで、その資産はトランザクションが終わるまでに返却される必要があります。
引用元:https://qiita.com/cardene/items/0158dd70155b165abf8d

アービトラージとは、安く買って高く売る取引手法のことで、
例えば

  1. 安いスーパーでお茶を買う(100円)
  2. すぐに高いコンビニで売る(150円)
  3. 差額の50円が儲けになる

といった流れとなります。

ポイントは「同時に」という点で、これは価格がすぐに変わる可能性があるためです。

今回はこの仕組みをブロックチェーン上で実行可能なスマートコントラクトを実装し、テストネットへデプロイしていきます。

仕様

フロントエンド

  • Next.js 15(App Router)
  • React
  • TanStack Query
  • Wagmi v2
  • Viem v2

ウォレットを接続するためのUI部分となります。
詳細は割愛しますが、Wagmi v2/Viem v2はウォレットとの接続や署名、トランザクションの送信などを簡単に行えます。

ブロックチェーン

スマートコントラクト

  • Solidity ^0.8.20
  • Hardhat
  • OpenZeppelin Contracts
  • Balancer V2
  • Uniswap V2/V3

参考実装

主要コンポーネント

  1. Balancerのフラッシュローン: 取引に必要な資金を一時的に借りるために使用
  2. Uniswap V3とSushiSwap: 異なる取引所間の価格差を利用してアービトラージを行う
  3. セキュリティ機能: ReentrancyGuardを使用して再入攻撃を防止
FlashLoanArbitrage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IFlashLoanRecipient.sol";
import "@balancer-labs/v2-interfaces/contracts/solidity-utils/openzeppelin/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";

主要機能

アービトラージ実行

  • executeArbitrage関数で開始
  • Balancerからフラッシュローンを借り入れ
  • Uniswap V3で取引を実行
  • SushiSwapで逆取引を実行
  • 利益を計算し、最小利益率をチェック
FlashLoanArbitrage.sol
function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) external override {
        require(msg.sender == address(vault), "Only Balancer Vault");

        // userData からパラメータをデコード
        ArbitrageParams memory params = abi.decode(userData, (ArbitrageParams));

        // 1. Uniswap V3での取引
        uint256 amountReceived = executeUniswapTrade(
            params.tokenIn,
            params.tokenOut,
            amounts[0],
            params.uniswapFee,
            params.deadline
        );

        // 2. SushiSwap V3での逆取引
        uint256 finalAmount = executeSushiswapTrade(
            params.tokenOut,
            params.tokenIn,
            amountReceived,
            params.deadline
        );

        // 利益の計算
        uint256 profit = finalAmount - (amounts[0] + feeAmounts[0]);
        require(profit > 0, "No profit");

        // 最小利益率のチェック
        uint256 profitBps = (profit * BPS_DENOMINATOR) / amounts[0];
        require(profitBps >= MIN_PROFIT_BPS, "Insufficient profit");

        // ローンの返済
        for (uint256 i = 0; i < tokens.length; i++) {
            IERC20(tokens[i]).transfer(
                address(vault),
                amounts[i] + feeAmounts[i]
            );
        }

        // 利益を所有者に送信
        if (profit > 0) {
            IERC20(params.tokenIn).transfer(owner, profit);
        }

        emit ArbitrageExecuted(
            _convertToAddressArray(tokens),
            amounts,
            profit
        );
    }

    function executeArbitrage(
        address tokenIn,
        address tokenOut,
        uint256 amount,
        uint24 uniswapFee,
        uint256 deadline
    ) external nonReentrant {
        require(msg.sender == owner, "Only owner");
        
        // 価格チェック
        (bool isProfitable,) = checkProfitability(
            tokenIn,
            tokenOut,
            amount,
            uniswapFee
        );
        require(isProfitable, "No profitable opportunity");

        // フラッシュローンのパラメータを準備
        IERC20[] memory tokens = new IERC20[](1);
        tokens[0] = IERC20(tokenIn);

        uint256[] memory amounts = new uint256[](1);
        amounts[0] = amount;

        // アービトラージパラメータのエンコード
        ArbitrageParams memory params = ArbitrageParams({
            tokenIn: tokenIn,
            tokenOut: tokenOut,
            amountIn: amount,
            uniswapFee: uniswapFee,
            deadline: deadline
        });

        // フラッシュローンの実行
        vault.flashLoan(
            this,
            tokens,
            amounts,
            abi.encode(params)
        );
    }

収益性チェック

  • checkProfitability関数で取引前に収益性を確認
  • Uniswapと SushiSwapの価格を比較
  • フラッシュローン手数料(0.02%)を考慮
FlashLoanArbitrage.sol
function checkProfitability(
        address tokenIn,
        address tokenOut,
        uint256 amount,
        uint24 uniswapFee
    ) public returns (bool, uint256) {
        // Uniswapでの予想取引結果
        uint256 uniswapAmount = getUniswapQuote(
            tokenIn,
            tokenOut,
            amount,
            uniswapFee
        );

        // SushiSwapでの予想取引結果
        uint256 sushiswapAmount = getSushiswapQuote(
            tokenOut,
            tokenIn,
            uniswapAmount
        );

        // Balancerのフラッシュローン手数料(0.02%)
        uint256 flashLoanFee = (amount * 2) / 10000;

        // 予想利益の計算
        if (sushiswapAmount > amount + flashLoanFee) {
            uint256 expectedProfit = sushiswapAmount - (amount + flashLoanFee);
            uint256 profitBps = (expectedProfit * BPS_DENOMINATOR) / amount;
            
            emit PriceChecked(
                tokenIn,
                tokenOut,
                uniswapAmount,
                sushiswapAmount
            );

            return (profitBps >= MIN_PROFIT_BPS, expectedProfit);
        }

安全性機能

  • オーナーのみが重要な操作を実行可能
  • 緊急出金機能(emergencyWithdraw)を実装
FlashLoanArbitrage.sol
function emergencyWithdraw(address token) external {
        require(msg.sender == owner, "Only owner");
        uint256 balance = IERC20(token).balanceOf(address(this));
        if (balance > 0) {
            IERC20(token).transfer(owner, balance);
        }
    }

デプロイ作業

1. 環境設定

.env
# Deployment
PRIVATE_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ウォレットの秘密鍵
POLYGONSCAN_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 確認用のAPIキー

# Contract Addresses (Polygon Amoy Testnet)
BALANCER_VAULT=0x...
UNISWAP_ROUTER=0x..
SUSHISWAP_ROUTER=0x...

2. デプロイ用スクリプトの準備

deploy.ts
import { ethers } from "hardhat";

async function main() {
  // Polygon Amoy Testnet addresses
  const BALANCER_VAULT = "0x...";
  const UNISWAP_ROUTER = "0x...";
  const SUSHISWAP_ROUTER = "0x...";

  console.log("Deploying FlashLoanArbitrage contract...");

  const FlashLoanArbitrage = await ethers.getContractFactory("FlashLoanArbitrage");
  const flashLoanArbitrage = await FlashLoanArbitrage.deploy(
    BALANCER_VAULT,
    UNISWAP_ROUTER,
    SUSHISWAP_ROUTER
  );

  await flashLoanArbitrage.waitForDeployment();

  const address = await flashLoanArbitrage.getAddress();
  console.log(`FlashLoanArbitrage deployed to: ${address}`);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  }); 

3. 必要なパッケージのインストール

npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle 

hardhatの導入方法は後日気が向いたら書こうと思います。

4. デプロイ作業

# コンパイル
npx hardhat compile

# Polygon Amoy Testnetへデプロイ
npx hardhat run scripts/deploy.ts --network amoy

5. 注意点

  • Balancer,Uniswap,Sushiswapのコントラクトアドレスは公式ドキュメントを参照
  • 他のテストネットと同様ですがデプロイ時のガス代に相当するトークンが必要となります
    今回の場合はFaucetから手に入れる必要があります
    参考:https://note.com/standenglish/n/n7f12bcb08d28

実際にデプロイされたコントラクト

おわりに

今回は「Polygon Amoy Testnetにスマートコントラクトをデプロイする」についてまとめてきました。
ええ...(困惑)
初記事としてこれはどうなの、、、

所感

Solidityやhardhatのおかげで比較的簡単にデプロイできるんだな~と
同時にまだまだインプットが足りないなぁと
初投稿という点については自身の備忘録として記事を書いてみて良かった気がする。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?