こんにちは、Fractonのリサーチエンジニアの池田です。
Solidityのフレームワークの一つであるFoundryの使い方を説明します。FoundryはCrypto VC大手のParadigmが開発している、Solidity開発フレームワークです。
テストコードをJavaScriptやTypeScriptではなくSolidityで記述するため、テストの記述が比較的楽であるという特徴があります。デバッグもやりやすく、今後のSolidity開発で役立つこと間違いなしのツールとして注目を集めています。
Foundryの使い方
公式のDocsはこちらです。 公式のDocsが非常に丁寧に書かれているため、これを見れば使い方はわかります。
またFoundryのtemplateとしてこちらのレポジトリーも参考になります。
openzeppelinのERC20をインポートして、テスト、デプロイをしており、Foundryの基本的な使い方が学べます。
環境構築
新規にFoundryプロジェクトを作るときは、以下のようにinitします。この場合は、hello_foundryディレクトリにプロジェクトが作成されます。
forge init hello_foundry
既存のFoundryプロジェクトを使うときは、git cloneしたあと、packageを以下のコマンドでインストールすれば使えるようになります。
forge install
forge installで新規にpackageをインストールしたい場合は、以下のようにします。これでOpenzeppelinのpackageが使えるようになります。なお、FoundryではパッケージのインストールにGIt Modulesが利用されています。
forge install OpenZeppelin/openzeppelin-contracts
テスト
テストコードはSolidityで書かれます。Hardhatよりもテストを高速に実行でき、体感的にもかなりサクサクにテストが実行できて快適です。
以下、公式Docsのサンプルを使って、使い方を見てみます。
pragma solidity 0.8.10; import "forge-std/Test.sol"; error Unauthorized(); contract OwnerUpOnly { address public immutable owner; uint256 public count; constructor() { owner = msg.sender; } function increment() external { if (msg.sender != owner) { revert Unauthorized(); } count++; } } contract OwnerUpOnlyTest is Test { OwnerUpOnly upOnly; function setUp() public { upOnly = new OwnerUpOnly(); } function testIncrementAsOwner() public { assertEq(upOnly.count(), 0); upOnly.increment(); assertEq(upOnly.count(), 1); } }
import "forge-std/Test.sol";
というFoundry標準のTestモジュールを使用します。このTestの中に、assertEq
, expectEmit
, expectRevert
などテストに必要な機能が全て入っています。上記の例では、OwnerUpOnlyがテストしたいコントラクトで、OwnerUpOnlyTestがテストコントラクトです。upOnly.increment();
によって、count()
が1になっていることをテストしています。
testコマンドは以下です。テストを実行すると、functionの名前がtestから始まるものがテストとして実行されます。今回の例では、testIncrementAsOwner()
です。
forge test
実行すると以下の結果が表示されます。
$ forge test Compiling 7 files with 0.8.10 Compiler run successful Running 1 test for test/OwnerUpOnly.t.sol:OwnerUpOnlyTest [PASS] testIncrementAsOwner() (gas: 29162) Test result: ok. 1 passed; 0 failed; finished in 723.90µs
またテストのログ機能も非常に便利です。v flagオプションにより、ログを出力することができます。vの数で、ログをどの深さまで出力するかを選べます。5個(-vvvvv)が最大で、これを実行するとテストで実行されたすべてのメソッドのトレースを出力できます。
forge test -vvvvv
上記の例でやってみると、count()、increment()などの、途中の実行結果が表示されるようになり、デバッグが非常にやりやすくなります。
[24661] OwnerUpOnlyTest::testIncrementAsOwner() ├─ [2262] OwnerUpOnly::count() │ └─ ← 0 ├─ [20398] OwnerUpOnly::increment() │ └─ ← () ├─ [262] OwnerUpOnly::count() │ └─ ← 1 └─ ← ()
Cast コマンド
castコマンドを使って、cliからethereum rpcにリクエストし、スマートコントラクトをcallしたり、transactionを送信したりできます。
以下のようにname()メソッドをDai Stablecoinに対してcallすると、name="Dai Stablecoin"が返ってきます。
$ cast call 0x6b175474e89094c44da98b954eedeac495271d0f "name()(string)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf Dai Stablecoin
totalSupply()を実行すれば、totalSupply=64255...が返ります。
$ cast call 0x6b175474e89094c44da98b954eedeac495271d0f "totalSupply()(uint256)" --rpc-url https://eth-mainnet.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf 6425577805499084133345931551
sendを使うとtransactionを送信できます。rinkebyのUSDCに対して、approve()を実行してみます。(address, uint256)=(0xdE62..., 1)をparamsとして実行してみます。
cast send 0xeb8f08a975Ab53E34D8a0330E0D34de942C95926 "approve(address, uint256)" 0xdE62195B5DF46D95D5349944E66E39d7C2a591f7 1 --rpc-url https://eth-rinkeby.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf --private-key=$PRIVATE_KEY
実行するとtransaction receiptが返ってきます。
blockHash 0x8cbb654e7f849c27d0cc6b24e73c81d0216f6afedde0024e8f808e721333c37e blockNumber 11002969 contractAddress cumulativeGasUsed 26211 effectiveGasPrice 3000000008 gasUsed 26211 logs [{"address":"0xeb8f08a975ab53e34d8a0330e0d34de942c95926","topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x000000000000000000000000de62195b5df46d95d5349944e66e39d7c2a591f7","0x000000000000000000000000de62195b5df46d95d5349944e66e39d7c2a591f7"],"data":"0x0000000000000000000000000000000000000000000000000000000000000001","blockHash":"0x8cbb654e7f849c27d0cc6b24e73c81d0216f6afedde0024e8f808e721333c37e","blockNumber":"0xa7e459","transactionHash":"0xddbf59b05b49b156a7272e7a08d87b8426aa0004a86400e8a8817f2f255916e4","transactionIndex":"0x0","logIndex":"0x0","removed":false}] logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000002000000000010000000000000000000001000000000000000000000010000000000000000000000000000000000000000000000000000000000040 root status 1 transactionHash 0xddbf59b05b49b156a7272e7a08d87b8426aa0004a86400e8a8817f2f255916e4 transactionIndex 0 type 2 cast send 0xeb8f08a975Ab53E34D8a0330E0D34de942C95926 "approve(address, uint256)" 0xdE62195B5DF46D95D5349944E66E39d7C2a591f7 1 --rpc-url https://eth-rinkeby.alchemyapi.io/v2/Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf --private-key=$PRIVATE_KEY
Anvil コマンド
Anvilコマンドはローカルノードを立ち上げるためのコマンドです。anvil
とcliに入力すると、以下のようにノードが立ち上がります。GanacheやHardhatノードよりもかなり高速に動作するようです。
$ anvil _ _ (_) | | __ _ _ __ __ __ _ | | / _` | | '_ \ \ \ / / | | | | | (_| | | | | | \ V / | | | | \__,_| |_| |_| \_/ |_| |_| 0.1.0 (8216a10 2022-06-01T00:04:28.362698536Z) https://github.com/foundry-rs/foundry Available Accounts ================== (0) 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH) (1) 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH) (2) 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH) (3) 0x90f79bf6eb2c4f870365e785982e1f101e93b906 (10000 ETH) (4) 0x15d34aaf54267db7d7c367839aaf71a00a2c6a65 (10000 ETH) (5) 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc (10000 ETH) (6) 0x976ea74026e726554db657fa54763abd0c3a0aa9 (10000 ETH) (7) 0x14dc79964da2c08b23698b3d3cc7ca32193d9955 (10000 ETH) (8) 0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f (10000 ETH) (9) 0xa0ee7a142d267c1f36714e4a8f75612f20a79720 (10000 ETH) Private Keys ================== (0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 (1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d (2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a (3) 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6 (4) 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a (5) 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba (6) 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e (7) 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356 (8) 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 (9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 Wallet ================== Mnemonic: test test test test test test test test test test test junk Derivation path: m/44'/60'/0'/0/ Base Fee ================== 1000000000 Gas Price ================== 20000000000 Gas Limit ================== 30000000 Listening on 127.0.0.1:8545
anvilはローカルノードだけではなく、実際に動いているネットワークをフォークしてローカルで実行できます。fork-urlオプションでmainnetを指定して実行します。
anvil --fork-url https://mainnet.infura.io/v3/$INFURA_KEY
環境変数をセットし、ALICE, 適当なUSERの残高を調べてみます。
export DAI=0x6b175474e89094c44da98b954eedeac495271d0f export USER=0xad0135af20fa82e106607257143d0060a7eb5cbf
USERの残高を呼び出すと、mainnet forkしたローカルネットワークにcallするので、mainetの残高を見ることができます。
$ cast call $DAI \ "balanceOf(address)(uint256)" \ $USER 71686045944718512103110072
まとめ
Foundryを使うとSolidityでテストコードがかけ、高速にテストが実行できます。cast, anvilなどのツールも充実しているので、今後Solidity開発になくてはならないツールとなると思います。