見出し画像

健全な借金 『フラッシュローン』 を攻略する

おはこんばんにちは。
わいです。

今回は、DEXbotterならお馴染みの『フラッシュローン』という便利関数を使用する方法と、実際のexampleCodeを踏まえて解説していきます。
(真面目で草)


??) なぜそんなこと公開するかって?

しれっとINFURAの服着てて草

まあ、実際フラッシュローンについて解説している記事が少ないので。。

出しているところもありますが、結局はドキュメントに書いているような内容を日本語に翻訳して復唱してるだけです。(全体の総意ではありません)

FlashLoan単体のexampleCodeなんて需要皆無なので、みなさんが望んでいるユースケースである、『フラッシュローンアービトラージ』のコントラも後に公開します。

僕はDEXbotをやめた身なので、全公開です!


1. フラッシュローンの概要

フラッシュローンとは、無担保無利子で大量のトークン借入ができる機能。
言わばクリプトだから可能であり、健全で且つ、合法的な借金手段です。

ただし、条件もしっかりあります。

借入返済のフローは同一のトランザクションでなければならない。
プロトコルによって、借入額の数%が手数料として掛かる。

この2つが大きな制約です。

各プロトコルの手数料率

Uniswap: 0.3%
AaveV2: 0.09%
AaveV3: 0.05%
dYdX: 0%
Balancer: 0%

といったところでしょうか。

数少ないリファレンスの中で多いのはAaveのフラッシュローン解説が多いですが、実際の利用割合として多いプロトコルは、Balancerとのこと。

https://eigenphi.io/mev/ethereum/flashloan

dYdXも同等に0%ですが、開発難易度が比較的高いらしくAave系統と酷似しているBalancerがやはり多かったです。

ワークフロー (シーケンス図)

基本的にAave, Balancerは上記のようなワークフローです。

executeFlashloan関数でPoolProvider / Vaultとの対話を行い、コントラクトへトークン借入を行ってOperationへ渡します。

この時、例として単一的な処理なので借入したら直ぐに返済しております。(実際にはここにロジを組んでスワップ処理入れたりなどします。)

そして、コントラ残高にある手数料分の借入トークンを加味してPoolProvider / Vaultへ返済します。

なお、結局はトークンmintのeventも発生していない = 既存コントラの残高分から借入しているため、競合に枠が奪われる可能性もあります。

MAX限度額 = Vault残高

ご利用は計画的に(意味深)


2. 単体の実装方法

本記事では、Balancerを使用した例を記載しております。
※ここからは自己責任でよろしくお願い致します

負いかねます!!!

実装手順

  1. RemixIDEにてコントラクトをデプロイ

  2. デプロイ済みコントラクトに借入トークンを少量預入

  3. executeFlashLoan関数を実行


下記のコードをデプロイ

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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";

contract BalancerSimpleFlashLoan is IFlashLoanRecipient {
    IVault private immutable vault;
    address public owner;

    constructor(IVault _vault) {
        vault = _vault;
        owner = msg.sender;
    }

    function executeFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        bytes memory userData
    ) external {
        vault.flashLoan(this, tokens, amounts, userData);
    }

    function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) external override {
        require(msg.sender == address(vault), "Unauthorized");
        
        // ここでFlashLoanで借りた資金を使用するロジックを実装
        // 現在は単純に借りた金額を返済するだけ

        for (uint256 i = 0; i < tokens.length; i++) {
            IERC20(tokens[i]).transfer(address(vault), amounts[i]);
        }
    }

    function withdrawToken(address tokenAddress) external {
        require(msg.sender == owner, "Only owner can Withdraw");
        IERC20 token = IERC20(tokenAddress);
        uint256 balance = token.balanceOf(address(this));
        require(balance > 0, "No Balance to Withdraw");
        require(token.transfer(owner, balance), "Transfer Failed");
    }

    function transferOwnership(address newOwner) external {
        require(msg.sender == owner, "Only owner can Transfer Wwnership");
        require(newOwner != address(0), "New owner cannot be Zero Address");
        owner = newOwner;
    }
}

今回は例としてUSDCをFlashLoanしていきます。

0.1USDCをコントラクトに預入(差し引かれませんが、実行するためのstateとして必要です。)

引数例

tokens: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"]
amounts: ["10000000000"]
userData: 0x

実行したら下記のようなTX構造になっているかと思います。

コントラにUSDCが転送され同枚数がそのまま返済

預け入れたトークンを脱出したい時は、withdrawToken関数にUSDCのアドレスを打ち込めばOKです。


3. 発展

※完全自己責任でお願い致します。

ということで、みなさんお待ちかねのフラッシュローンアービトラージのコントラです。

現時点ではv2 / fork系統のみ対応しているので、v3の場合は関数ロジが変わるため随時修正してください。

また、revert処理を考慮していないため、実際に稼働させる際は上手い具合にrevert処理を追加してください。

実コード

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

//import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; # safeERC20ライブラリへ移行
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

interface IVault {
    function flashLoan(
        IFlashLoanRecipient recipient,
        IERC20[] memory tokens,
        uint256[] memory amounts,
        bytes memory userData
    ) external;
}

interface IFlashLoanRecipient {
    function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory userData
    ) external;
}

interface IRouter {
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);

    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);
}

contract FlashLoanArbitrageurV2 is IFlashLoanRecipient, ReentrancyGuard, Ownable {
    using SafeERC20 for IERC20;

    IVault private immutable vault;

    event LoanExecuted(uint256 borrowed, uint256 returned, uint256 profit);
    event BalanceInfo(uint256 initialBalance, uint256 finalBalance, uint256 amountToRepay);
    event SwapExecuted(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 amountOutMin);
    event ArbitrageStepCompleted(uint256 step, uint256 currentAmount);
    event ETHWithdrawn(address to, uint256 amount);
    event EmergencyExit(address token, uint256 amount);

    constructor(address _vault) Ownable(msg.sender) {
        vault = IVault(_vault);
    }

    struct ArbitrageParams {
        address[] tokens;
        address[] routers;
        bool[] useFeeOnTransfer;
        uint256[] slippageTolerances;
    }

    function executeFlashLoanArbitrage(
        address assetToBorrow,
        uint256 amountToBorrow,
        address[] calldata tokens,
        address[] calldata routers,
        bool[] calldata useFeeOnTransfer,
        uint256[] calldata slippageTolerances
    ) external onlyOwner nonReentrant {
        require(tokens.length == routers.length + 1, "Invalid Tokens Length");
        require(routers.length == useFeeOnTransfer.length, "Invalid Routers or useFeeOnTransfer Length");
        require(routers.length == slippageTolerances.length, "Invalid slippageTolerances Length");
        
        ArbitrageParams memory params = ArbitrageParams(tokens, routers, useFeeOnTransfer, slippageTolerances);
        bytes memory encodedParams = abi.encode(params);

        IERC20[] memory assets = new IERC20[](1);
        assets[0] = IERC20(assetToBorrow);

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

        vault.flashLoan(this, assets, amounts, encodedParams);
    }

    function receiveFlashLoan(
        IERC20[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory /* feeAmounts */,
        bytes memory userData
    ) external override {
        require(msg.sender == address(vault), "Caller Must be Balancer Vault");

        ArbitrageParams memory arbitrageParams = abi.decode(userData, (ArbitrageParams));

        uint256 amountToRepay = amounts[0];
        uint256 initialBalance = tokens[0].balanceOf(address(this));

        emit BalanceInfo(initialBalance, 0, amountToRepay);

        uint256 amountReturned = _executeArbitrage(amounts[0], arbitrageParams);

        uint256 finalBalance = tokens[0].balanceOf(address(this));
        emit BalanceInfo(initialBalance, finalBalance, amountToRepay);

        if (finalBalance >= amountToRepay) {
            tokens[0].safeTransfer(address(vault), amountToRepay);
            
            uint256 profit = finalBalance - amountToRepay;
            if (profit > 0 && finalBalance > amountToRepay + profit) {
                tokens[0].safeTransfer(owner(), profit);
            }
            
            emit LoanExecuted(amounts[0], amountReturned, profit);
        } else {
            revert("Not Enough Funds to Repay FlashLoan");
        }
    }

    function _executeArbitrage(uint256 amount, ArbitrageParams memory params) internal returns (uint256) {
        uint256 currentAmount = amount;
        for (uint i = 0; i < params.routers.length; i++) {
            currentAmount = _swapTokens(
                params.tokens[i],
                params.tokens[i+1],
                currentAmount,
                params.routers[i],
                params.useFeeOnTransfer[i],
                params.slippageTolerances[i]
            );
            emit ArbitrageStepCompleted(i, currentAmount);
        }

        return currentAmount;
    }

    function _swapTokens(
        address tokenIn,
        address tokenOut,
        uint256 amountIn,
        address router,
        bool useFeeOnTransfer,
        uint256 slippageTolerance
    ) internal returns (uint256) {
        require(slippageTolerance <= 10000, "Slippage Tolerance Must be <= 100%");
        IERC20(tokenIn).safeIncreaseAllowance(router, amountIn);

        address[] memory path = new address[](2);
        path[0] = tokenIn;
        path[1] = tokenOut;

        uint256 amountOutMin = amountIn * (10000 - slippageTolerance) / 10000;

        uint256 amountOut;
        if (useFeeOnTransfer) {
            IRouter(router).swapExactTokensForTokensSupportingFeeOnTransferTokens(
                amountIn,
                amountOutMin,
                path,
                address(this),
                block.timestamp
            );
            amountOut = IERC20(tokenOut).balanceOf(address(this));
        } else {
            uint[] memory amounts = IRouter(router).swapExactTokensForTokens(
                amountIn,
                amountOutMin,
                path,
                address(this),
                block.timestamp
            );
            amountOut = amounts[amounts.length - 1];
        }

        emit SwapExecuted(tokenIn, tokenOut, amountIn, amountOut, amountOutMin);

        return amountOut;
    }

    function withdrawToken(address token, uint256 amount) external onlyOwner nonReentrant {
        uint256 balance = IERC20(token).balanceOf(address(this));
        require(balance >= amount, "Insufficient Balance");
        IERC20(token).safeTransfer(owner(), amount);
    }

    function withdrawETH(address payable to) external onlyOwner nonReentrant {
        uint256 balance = address(this).balance;
        require(balance > 0, "No ETH Balance");
        (bool success, ) = to.call{value: balance}("");
        require(success, "ETH Transfer Failed!!!");
        emit ETHWithdrawn(to, balance);
    }

    // 救済用関数
    function emergencyExit(address token) external onlyOwner nonReentrant {
        uint256 balance;
        if (token == address(0)) {
            balance = address(this).balance;
            (bool success, ) = owner().call{value: balance}("");
            require(success, "ETH Transfer Failed");
        } else {
            balance = IERC20(token).balanceOf(address(this));
            IERC20(token).safeTransfer(owner(), balance);
        }
        emit EmergencyExit(token, balance);
    }

    receive() external payable {}
}

本来ならば、finalBalanceがinitialBalanceより大きい値でないと実質的に負けているので、revert処理を実装しましょう。

ワークフロー (シーケンス図)

このTX例として、PancakeSwapV2とUniswapV2間の二者間アビトラの図です。

割とシンプルで、FlashLoanArbitrageurコントラクトがBalancer VaultからUSDCをFlashLoanし、PancakeSwapV2にてwETHへスワップ。

そしてUniswapV2にてwETHをUSDCに戻して、initialBalanceより減っていたらコントラ残高から補填してrepay

もし、利益が出た場合にはuserへ利益分がtransferされます。

三者間アビトラやルートパスを増やしたい方は、それぞれのRouterアドレスを取得して設計してください。

ここで重要なのが、実際に稼働させる際はNodejsやPythonで監視兼実行システムを作成し、利鞘が取れそうなタイミングを測って実行させましょう。
(人力じゃ無理です)

監視については前に書いたnoteにてDEX上での価格取得のやり方を記載しているので、そちらを参考にしてみてください!


5. 最後に+ 余談

具体例やコードも公開してFlashLoanについての解説も行いました。

割とFlashLoanは使えると相当便利な関数なので、この機会にチャレンジしてみてください!

他にはレンディング系でLiquidationする際にも使えるかと思いますので、時間があったらやってみようかと思います。

もし良ければ、投げ銭してくださると喜びます。


また、少し余談ですがDEX / CEX 共にBOT開発のリクエストなど受けようかと思っております。

儲かるかどうか分からないため、利益折半などは考えていないのですが。
単純にロジック制作がメインとして考えております。

ご要望ありましたら下記のGoogleFormにてご連絡ください。
(出来る限りなるはやで返します)


参考文献

この記事が気に入ったらサポートをしてみませんか?