Flash Loanに挑戦@Solidity

フラッシュローン(Flash Loan)とは、分散型金融(DeFi)の仕組みの一つで無担保で借り入れを行い、同じトランザクション内で返済を完了するローン。
借りた資金は同じトランザクション内で必ず返済する必要がある。
今回は、SepoliaネットワークでAAVEから借り入れして何もせずに返済するというコードを作成した。

※著者の勉学目的で書いたコードですので、バグなどが含まれる可能性があります。


コード完成までの流れ

コード作成の流れ

まずは、Flash Loanのコードが落ちていないかをネット検索した。

すると、それっぽいものが見つかった。
https://metana.io/blog/how-to-make-a-flash-loan-using-aave/

ただ、これだけでは何をすればよいかが分からない。
importもあるのでライブラリのインストールも必要になりそうだ。

ChatGPTに、このコードを貼り付けて「これを実装したいのですがどうすれば良いですか?」と聞きました。

すると、Node.jsとHardhatを使った実装を教えてくれました。ライブラリのインストールについてもきちんと指示がありました。これはいけそうだ。

ただ、私はfoundryを使って実装したいのでその旨を追加で伝える。

同様の軌跡をたどりたい場合は過去記事をご覧ください。

すると、Aave V3ライブラリのインストールコマンドの指示と、src/SimpleFlashLoan.solのコードも提示があった。

forge install aave/core-v3
forge install OpenZeppelin/openzeppelin-contracts
// SPDX-License-Identifier: MIT
pragma solidity 0.8.10;

import {FlashLoanSimpleReceiverBase} from "aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPoolAddressesProvider} from "aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleFlashLoan is FlashLoanSimpleReceiverBase {

    constructor(address _addressProvider) FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider)) {}

    function flashLoan(address _token, uint256 _amount) public {
        address receiverAddress = address(this); 
        address asset = _token;
        uint256 amount = _amount; 
        bytes memory params = "";
        uint16 referralCode = 0;

        POOL.flashLoanSimple(receiverAddress, asset, amount, params, referralCode);
    }

    function executeOperation(
        address asset, 
        uint256 amount, 
        uint256 premium, 
        address initiator, 
        bytes calldata params
    ) external override returns (bool) {
        uint256 totalAmount = amount + premium; 
        IERC20(asset).approve(address(POOL), totalAmount);
        return true;
    }
}

ただ、aave/core-v3のインストールで早速エラー。

これもエラー内容をコピーしてChatGPTに伝える。

すると、以下が正しいと修正が入る。

forge install aave/protocol-v3

これに伴って、Solidityコードのimportパスも修正になる(最終的な完成コードは後述)。

このChatGPTとのやり取りを繰り返すことで、ひとまずforge buildが通るコードにはなる。

次に、Flash Loanの動きを再確認する(これもChatGPTに理解できるまで壁打ちすればOK)。

Flash Loanの動き

  1. フロントエンドやスクリプトでflashLoanメソッドを呼び出す

  2. 適切なERC20トークンアドレス(例:WETHやDAI)と借りたい額を指定

  3. Flash Loanを成功させるには、借りた資金に「プレミアム」を上乗せして返済する必要がある

プレミアムとは、借入額に対して支払う手数料のことを指す。Aaveでは、フラッシュローンの手数料はChatGPTによると通常0.09%らしいが、後述する実際の実行では0.05%のように見える。
どちらにせよプレミアムを乗せて返済する必要があるということ。

プレミアムの準備について

プレミアムの元本をどのように確保するかについては、executeOperationの実装内容に依存するとのこと。

なので、SimpleFlashLoanコントラクトを呼び出したユーザーが支払うことも可能なのかもしれないが、今回はSimpleFlashLoanコントラクトが支払うこととする。

となると、支払方法は以下の2つになる。

  1. Flash Loanを利用して得た利益の中から支払う(アービトラージなど)。

  2. あらかじめコントラクトにプレミアム用の資金を送金しておく。

今回の実装では、借りたものをそのまま返すので2を選ぶことになる。

となると、資金をあらかじめコントラクトに送金することになり、これを引き出す関数も用意しておかなければ資金をセルフGOXしてしまう。
また、コントラクトに資金を置くということは、関数の操作を自分自身に限定しておかなければ他人に資金を奪われる可能性が出てくる。

ということで、コードにその対策をするようにChatGPTに要請。

  • onlyOwner()というmodifierの設定でSimpleFlashLoanコントラクト作成者にその操作を限定

  • withdraw関数の設定で預けた資金を引き出せるように変更

完成コード

src/SimpleFlashLoan.sol

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

import {FlashLoanSimpleReceiverBase} from "protocol-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPoolAddressesProvider} from "protocol-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SimpleFlashLoan is FlashLoanSimpleReceiverBase {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not authorized: only owner can call this function");
        _;
    }

    constructor(address _addressProvider) FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider)) {
        owner = msg.sender; // コントラクトのデプロイ者を作成者として記録
    }

    // フラッシュローンをリクエストする関数
    function flashLoan(address _token, uint256 _amount) public onlyOwner {
        address receiverAddress = address(this); 
        address asset = _token;
        uint256 amount = _amount; 
        bytes memory params = "";
        uint16 referralCode = 0;

        POOL.flashLoanSimple(receiverAddress, asset, amount, params, referralCode);
    }

    // フラッシュローン実行中に呼び出されるコールバック関数
    function executeOperation(
        address asset, 
        uint256 amount, 
        uint256 premium, 
        address initiator, 
        bytes calldata params
    ) external override returns (bool) {
        uint256 totalAmount = amount + premium; 
        IERC20(asset).approve(address(POOL), totalAmount);
        return true;
    }

    // 作成者が預けた資金を引き出す
    function withdrawFunds(address _token, uint256 _amount) external onlyOwner {
        IERC20(_token).transfer(owner, _amount);
    }

    // 作成者がETHを引き出す
    function withdrawETH(uint256 _amount) external onlyOwner {
        payable(owner).transfer(_amount);
    }

    // コントラクトにETHを送金するためのfallback
    receive() external payable {}
}
  • flashLoan関数を通じて、Aaveのプールから資金が借りられ、このコントラクト(SimpleFlashLoan)に送金される

  • executeOperation関数で、借りた資金を使ってアービトラージ、清算、その他の操作を実行する。この関数の中で、借りた金額(amount)とプレミアム(premium)をAaveプールに返済する。

  • POOL.flashLoanSimple関数が、内部的にexecuteoperation関数を呼び出している。

asset:借りたトークンのアドレス
amount:借りた金額
premium:フラッシュローンの手数料
initiator:フラッシュローンの発行者(通常はこのコントラクト)
params:追加データ(今回は未使用)。

ちなみに、ガス代はトランザクションを実行したユーザー(私)が支払います。

デプロイ(to Sepolia ETHネットワーク)

forge create --broadcast --rpc-url https://sepolia.infura.io/v3/<my_api_key> --private-key <my_private_key> src/SimpleFlashLoan.sol:SimpleFlashLoan --constructor-args 0x012bAC54348C0E635dCAc9D5FB99f06F24136C9A

--constructor-argsに続く引数は、Sepolia ETHネットワークのAaveのPoolAddressesProviderのアドレスです。

<my_api_key>はinfuraで取得したAPIキーです。

実際のトランザクション

https://sepolia.etherscan.io/tx/0x35686e5e7e9a1cff3a59d0e2876d980fda850f46b6f5d3c78d8548a7535fb5c8

ABI

ABIはSolidityコンパイル時にここに作成されたjsonファイルに書いてあります。

動作確認(@python)

SimpleFlashLoanコントラクトにあらかじめWETHを送金し、フラッシュローンを実行した結果を確認する。

#test.py

from web3 import Web3
from eth_abi import encode
from datetime import datetime
import json
import time

my_wallet_address = 'my_wallet_address'
my_private_key = 'my_private_key'
WETH = '0xC558DBdd856501FCd9aaF1E62eae57A9F0629a3c'
flashloan = '0x1E8F01F0A58Cb6d0648CACB4ED6806917830335c'

#Web3のセットアップ(Unichain Sepolia)
w3 = Web3(Web3.HTTPProvider("https://sepolia.infura.io/v3/73b4b974075049ca816618699c70d8e9"))

#コントラクトインスタンスの作成
with open('WETH_ABI.json') as f:
   wethABI = json.load(f)
weth_contract = w3.eth.contract(address=WETH, abi=wethABI)

#トランザクションの作成(deposit:WETH)
tx1 = weth_contract.functions.deposit().build_transaction({
    'value': w3.to_wei(0.01, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

#トランザクションの表示
#print(tx1)

# トランザクションの署名と送信
signed_tx1 = w3.eth.account.sign_transaction(tx1, private_key=my_private_key)
tx1_hash = w3.eth.send_raw_transaction(signed_tx1.raw_transaction)

print('deposit:WETH Wrap transaction')
print(w3.to_hex(tx1_hash))
result = w3.eth.wait_for_transaction_receipt(tx1_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

time.sleep(60)

#flashloan contractのWETH balanceの確認
balance1 = weth_contract.functions.balanceOf(flashloan).call()
print("balance of flashloan contract(WEHT): " + str(balance1/(10**18)))

#flashloan contractへWETHを送金
tx2 = weth_contract.functions.transfer(flashloan, w3.to_wei(0.01, 'ether')).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

#トランザクションの表示
#print(tx2)

# トランザクションの署名と送信
signed_tx2 = w3.eth.account.sign_transaction(tx2, private_key=my_private_key)
tx2_hash = w3.eth.send_raw_transaction(signed_tx2.raw_transaction)

print('transfer WETH to flashloan contract transaction')
print(w3.to_hex(tx2_hash))
result = w3.eth.wait_for_transaction_receipt(tx2_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

time.sleep(60)

#flashloan contractのWETH balanceの確認
balance2 = weth_contract.functions.balanceOf(flashloan).call()
print("balance of flashloan contract(WEHT): " + str(balance2/(10**18)))


#コントラクトインスタンスの作成
with open('loan_ABI.json') as f:
   loanABI = json.load(f)
flashloan_contract = w3.eth.contract(address=flashloan, abi=loanABI)

#flashloanで0.01WETH借りてそのまま返す(プレミアム分損する)
tx3 = flashloan_contract.functions.flashLoan(WETH, w3.to_wei(0.01, 'ether')).build_transaction({
    'value': w3.to_wei(0, 'ether'),
    'gas': 2000000,
    'gasPrice': w3.eth.gas_price,
    'nonce': w3.eth.get_transaction_count(my_wallet_address)
})

#トランザクションの表示
#print(tx3)

# トランザクションの署名と送信
signed_tx3 = w3.eth.account.sign_transaction(tx3, private_key=my_private_key)
tx3_hash = w3.eth.send_raw_transaction(signed_tx3.raw_transaction)

print('flashloan transaction')
print(w3.to_hex(tx3_hash))
result = w3.eth.wait_for_transaction_receipt(tx3_hash)
status = result['status']
if status == 1:
   print('Transaction Succeeded')
else:
   print('Transaction Failed')

time.sleep(60)

#flashloan contractのWETH balanceの確認
balance3 = weth_contract.functions.balanceOf(flashloan).call()
print("balance of flashloan contract(WEHT): " + str(balance3/(10**18)))

このコードの内容は以下です。

  1. ETHをWETHにWRAP

  2. WETHをSimpleFlashLoanコントラクトに送金

  3. flashLoan関数を実行

各ステップの後に、SimpleFlashLoanコントラクトが保持しているWETHの量を確認するようにしています。
また、それぞれtransactionが承認された後に確認したかったので、ざっくり1分待つ(time.sleep(60))ようにしています。

実行結果は以下です(部分的に実行しながら作成を進めたので、綺麗な数字にはなっていません)。

まず、Wrap transactionが発行されたタイミングでは、コントラクトは0.02WETH持っています。
0.01WETHをコントラクトに送金した後は、コントラクトは0.03WETH持っています。想定通りです。
最後に、flashLoan関数実行(0.01WETH借りてプレミアムを乗せて返す)後は、0.029995WETHとなっています。
→ここから、0.01WETH借りることに対してプレミアムは0.000005WETHだったことが分かり、借入額の0.05%がプレミアムなのでは?と考えています。

flashLoan関数実行部分のトランザクション

https://sepolia.etherscan.io/tx/0xa3dcb0c8b6019f27d991a3d84ba0ecfe4e107880c19b05a724cf508984842015

これを見ると、
ユーザー(私)がSimpleFlashLoanコントラクト(0x1E8F01F0A58Cb6d0648CACB4ED6806917830335c)に指示を出して
このコントラクトがあるアドレス(0x5b071b590a59395fE4025A0Ccc1FcC931AAc1830)から0.01WETHを借りて、0.010005ETHを返す、という流れが分かる。

ここで、0x5b071b590a59395fE4025A0Ccc1FcC931AAc1830、このアドレスは何?となったのでこれもChatGPTに聞いてみました。

指定されたアドレス 0x5b071b590a59395fE4025A0Ccc1FcC931AAc1830 は、Sepolia テストネット上の Aave プロトコルの WETH(aEthWETH)トークンコントラクトです。

とのことです。つまり、AaveのWETHプールから借りて、返した、という感じです。

今回は、借りて返しただけですが、この借りた資金を使って何かやってから返す、という風にすれば拡張性が期待できます。



いいなと思ったら応援しよう!