Fracton テックブログ

FractonのEthereum周辺技術について発信しています

Solidity開発ツール、Foundryの紹介

こんにちは、Fractonのリサーチエンジニアの池田です。

Solidityのフレームワークの一つであるFoundryの使い方を説明します。FoundryはCrypto VC大手のParadigmが開発している、Solidity開発フレームワークです。

テストコードをJavaScriptやTypeScriptではなくSolidityで記述するため、テストの記述が比較的楽であるという特徴があります。デバッグもやりやすく、今後のSolidity開発で役立つこと間違いなしのツールとして注目を集めています。

Foundryの使い方

公式のDocsはこちらです。 公式のDocsが非常に丁寧に書かれているため、これを見れば使い方はわかります。

book.getfoundry.sh

またFoundryのtemplateとしてこちらのレポジトリーも参考になります。

github.com

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コマンドはローカルノードを立ち上げるためのコマンドです。anvilcliに入力すると、以下のようにノードが立ち上がります。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開発になくてはならないツールとなると思います。