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

## はじめに
初投稿です！
普段は太陽光発電の販売会社で経理や情シスをやりつつ、スタートアップで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はウォレットとの接続や署名、トランザクションの送信などを簡単に行えます。

### ブロックチェーン
- Polygon Amoy Testnet
TestnetがMumbaiからAmoyへ切り替わっています。
参考：[Polygonのテストネット利用(接続)で2024年5月以降注意すること](https://qiita.com/Y4na9i8/items/e1785c56c3d63a82d1ae)

### スマートコントラクト
- 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. 必要なパッケージのインストール**
```shell
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle 
```
hardhatの導入方法は後日気が向いたら書こうと思います。

**4. デプロイ作業**
```shell
# コンパイル
npx hardhat compile

# Polygon Amoy Testnetへデプロイ
npx hardhat run scripts/deploy.ts --network amoy
```
**5. 注意点**
- Balancer,Uniswap,Sushiswapのコントラクトアドレスは公式ドキュメントを参照
- 他のテストネットと同様ですがデプロイ時のガス代に相当するトークンが必要となります
今回の場合は[Faucet](https://faucet.polygon.technology)から手に入れる必要があります
参考：https://note.com/standenglish/n/n7f12bcb08d28

実際にデプロイされた[コントラクト](https://amoy.polygonscan.com/address/0x0e6c5e7BCdb38634211D0FE0dcc6dE2625a72F31#code)


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

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