Foundry フォークテスト
まずはFoundry のfork テストをみてみましょう。こちらのyoutubeで分かりやすく解説されています。
https://www.youtube.com/watch?v=eKxJZgp9CTg
fork することで実際にデプロイされているコントラクトを使用できます。
forge test
でテストスクリプトでやるときは引数に指定してあげます。
forge test --fork-url <Alchemyなどのmainnet PRC> --match-path test/Weth.t.sol -vvvv
もしくは unvil をつかってローカルにforkのテストネットを構築する場合は以下で試せます。
anvil --fork-url <Alchemyなどのmainnet PRC>
wethのフォークテスト
先ほどのリンクのyoutube ではdeposit だけでしたが approve, transferfrom なども試してみました。
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "forge-std/console.sol";
interface IWETH {
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
function deposit() external payable;
function withdraw(uint256) external;
function totalSupply() external returns (uint256);
function approve(address, uint256) external returns (bool);
function transfer(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}
contract WethTest is Test {
IWETH public weth;
function setUp() public {
weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
}
function testWeth() public {
uint256 mybalance;
uint256 bob_balance;
uint256 alice_balance;
uint256 new_allowance;
address mywallet = address(this);
address bob = address(0x10000);
address alice = address(0x2000);
mybalance = weth.balanceOf(mywallet);
console.log("My balance before", mybalance);
weth.deposit{value: 10 ether}();
mybalance = weth.balanceOf(mywallet);
console.log("My balance after deposit", mybalance);
weth.transfer(bob, 1 ether);
mybalance = weth.balanceOf(mywallet);
bob_balance = weth.balanceOf(bob);
console.log("My balance after sending Bob 1 ether ", mybalance);
console.log("bob's balance", bob_balance);
console.log("approve Bob for 2 ether");
weth.approve(bob, 2 ether);
new_allowance = weth.allowance(mywallet, bob);
console.log("Bob's allowance from my wallet ", new_allowance);
bob_balance = weth.balanceOf(bob);
console.log("bob's balance", bob_balance);
console.log("Bob send Alice 2 ether instead of me");
vm.prank(bob);
weth.transferFrom(mywallet, alice, 2 ether);
alice_balance = weth.balanceOf(alice);
console.log("alice balance after transferfrom ", alice_balance);
mybalance = weth.balanceOf(mywallet);
console.log("My balance after transferfrom", mybalance);
uint256 total_supply = weth.totalSupply();
console.log("Total Supply ", total_supply);
}
}
結果はこんなかんじ
root@DESKTOP-N2O3OSL:~/weth# forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/xxxxx --match-path test/Weth.t.sol -vvv
[⠆] Compiling...
[⠒] Compiling 1 files with 0.8.22
[⠢] Solc 0.8.22 finished in 684.08ms
Compiler run successful!
Running 1 test for test/Weth.t.sol:WethTest
[PASS] testWeth() (gas: 115853)
Logs:
My balance before 0
My balance after deposit 10000000000000000000
My balance after sending Bob 1 ether 9000000000000000000
bob's balance 1000000000000000000
approve Bob for 2 ether
Bob's allowance from my wallet 2000000000000000000
bob's balance 1000000000000000000
Bob send Alice 2 ether instead of me
alice balance after transferfrom 2000000000000000000
My balance after transferfrom 7000000000000000000
Total Supply 3124229408761989594689998
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.16s
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
root@DESKTOP-N2O3OSL:~/weth#
uniswap v3でtokenを交換してみる。
uniscwap v3の調査
- 公式ドキュメントのスワップ交換に関するところ
https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps - 上記の元にした日本語のページ
https://zenn.dev/heku/books/bb6dd5fe02feb7/viewer/4d4b5a - uniswap v3 router のetherscan のページ
https://etherscan.io/address/0xe592427a0aece92de3edee1f18e0157c05861564
uniswap v3でtoken交換する関数はexactInputSingle(ExactInputSingleParams calldata)
とexactOutputSingle(ExactOutputSingleParams calldata)
でした。
でも公式ドキュメントでは自分でexample.solを作ってそこからuniswapのコントラクトを呼んでいる。やっていることは 出金元のtokenのコントラクトでapprove(<uniswap address>, amout>
してからuniswapのコントラクトの関数を呼んでいる。公式には実際のサンプルはないんですよね。以下のリンクは関数名は違うけどそのサンプルと同様にコントラクトを実際につくってやっている。Foundryのテストケースもある。
https://solidity-by-example.org/defi/uniswap-v3-swap/
なんでこんな面倒なことするんだろう。そのままexactInputSingle(ExactInputSingleParams calldata)
とexactOutputSingle(ExactOutputSingleParams calldata)
を呼んだほうがよさそうなので、foundryのテストから呼ぶことにしました。
uniswap v3 のトークン交換テスト
以下が使用したトークン交換のテストです。
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "forge-std/console.sol";
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
struct ExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountOut;
uint256 amountInMaximum;
uint160 sqrtPriceLimitX96;
}
interface IUNI {
function exactInputSingle(ExactInputSingleParams calldata) external payable returns (uint256 amountOut);
function exactOutputSingle(ExactOutputSingleParams calldata) external payable returns (uint256 amountOut);
}
interface IWETH {
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
function deposit() external payable;
function withdraw(uint256) external;
function totalSupply() external returns (uint256);
function approve(address, uint256) external returns (bool);
function transfer(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
}
interface IDAI {
function balanceOf(address) external view returns (uint256);
}
address constant WETH_ADDR=address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
address constant DAI_ADDR=address(0x6B175474E89094C44Da98b954EedeAC495271d0F);
address constant UNI_ADDR=address(0xE592427A0AEce92De3Edee1F18E0157C05861564);
contract UniswapTest is Test {
IWETH public weth;
IDAI public dai;
IUNI public uni;
function setUp() public {
weth = IWETH(WETH_ADDR);
dai = IDAI(DAI_ADDR);
uni = IUNI(UNI_ADDR);
}
function testExactInputSingle() public {
uint256 mybalance;
uint256 dai_balance;
uint256 amountOut;
address mywallet = address(this);
weth.deposit{value: 10 ether}();
mybalance = weth.balanceOf(mywallet);
console.log("My balance after deposit", mybalance);
dai_balance=dai.balanceOf(mywallet);
console.log("Dai Balance", dai_balance);
console.log("Approve Uni to spend 1 ether");
weth.approve(UNI_ADDR, 1 ether);
console.log("Swap 1 ether to Dai with exactInputSingle function");
console.logBytes4(uni.exactInputSingle.selector); // check selector for debug
amountOut=uni.exactInputSingle(ExactInputSingleParams(WETH_ADDR,DAI_ADDR,3000,mywallet,block.timestamp + 100,1 ether,0,0));
console.log("Amount Out", amountOut);
dai_balance=dai.balanceOf(mywallet);
console.log("Dai Balance (daller)", dai_balance/ (1 ether ));
}
function testExactOutputSingle() public {
uint256 mybalance;
uint256 dai_balance;
uint256 amountOut;
address mywallet = address(this);
weth.deposit{value: 10 ether}();
mybalance = weth.balanceOf(mywallet);
console.log("My balance after deposit", mybalance);
dai_balance=dai.balanceOf(mywallet);
console.log("Dai Balance", dai_balance);
console.log("Approve Uni to spend 5 ether");
weth.approve(UNI_ADDR, 5 ether);
console.log("Swap ether to 10000$ Dai with exactOutputSingle function");
amountOut=uni.exactOutputSingle(ExactOutputSingleParams(WETH_ADDR,DAI_ADDR,3000,mywallet,block.timestamp + 100, 10000 ether,5 ether,0));
console.log("Amount Out", amountOut);
dai_balance=dai.balanceOf(mywallet);
console.log("Dai Balance ", dai_balance/(1e16));
mybalance = weth.balanceOf(mywallet);
console.log("My ether balance", mybalance);
}
}
実行結果
root@DESKTOP-N2O3OSL:~/weth# forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/xxxxx --match-path test/Uniswap.t.sol -vvv
[⠆] Compiling...
[⠢] Compiling 1 files with 0.8.22
[⠆] Solc 0.8.22 finished in 739.49ms
Compiler run successful!
Running 2 tests for test/Uniswap.t.sol:UniswapTest
[PASS] testExactInputSingle() (gas: 145252)
Logs:
0x414bf389
My balance after deposit 10000000000000000000
Dai Balance 0
Approve Uni to spend 1 ether
Swap 1 ether to Dai with exactInputSingle function
Amount Out 2048503466360154622352
Dai Balance (daller) 2048
[PASS] testExactOutputSingle() (gas: 178964)
Logs:
0x414bf389
My balance after deposit 10000000000000000000
Dai Balance 0
Approve Uni to spend 5 ether
Swap ether to 10000$ Dai with exactOutputSingle function
Amount Out 4882008713537451517
Dai Balance 10000
My ether balance 5117991286462548483
Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.71s
Ran 1 test suites: 2 tests passed, 0 failed, 0 skipped (2 total tests)
testExactInputSingle() の説明
1. weth.approve(UNI_ADDR, 1 ether);
で uniswapに 1etherの使用許可を出します。
2. uni.exactInputSingle(...
にて実際のtoken交換を行います。
3. 1etherを最大量のDAIに交換します。
testExactInputSingle() の説明
1. weth.approve(UNI_ADDR, 5 ether);
で uniswapに 5etherの使用許可を出します。
2. uni.exactOutputSingle(...
にて実際のtoken交換を行います。
3. 許容量(5 ether) の範囲内で10,000 DAIに交換します。許容量が足りない場合はエラーになります。
######はまったとこと注意点
-
exactInputSingle(..
の呼び出しselector が正しいかどうかをconsole.logBytes4(uni.exactInputSingle.selector);
で確認した。実際のセレクターのIDはetherscanのところで確認できる。
-
ExactInputSingleParams
とExactOutputSingleParams
のfee
はきっちり3000
(0.3%)にしないと失敗した。これが時期による変動値なのかも不明です。 - 当然だけど
ExactOutputSingleParams
のamountInMaximum
に必要とされるトークン以下を指定するとエラーになります。