🦉

EthereumのDapp開発環境のFoundryを使ってみる

2022/07/09に公開
2

Foundryとは

FoundryはhardhatのようなDappの開発環境を提供してくれるRust製のツール。開発・テスト・デプロイなどのコードをSolidityで全て書けて便利。

インストールは下記で行う。

 curl -L https://foundry.paradigm.xyz | bash
 foundryup

これで3つのツールが使えるようになる。

  • Forge: アプリのセットアップ/ビルド/テストを行うためのツール
  • Cast: スマートコントラクトとの通信を行うクライアント
  • Anvil: ローカル開発用のローカルノード

まさしくHardhatやTruffle&Ganacheの代替となるようなツールである。

使ってみる

理解するにはまずは使ってみるのが早そうということで以下ではopenzeppelin-contractsを使って簡単なNFTのコントラクトを書いていく。その過程でスマートコントラクトの作成からテスト、ローカルへのデプロイまでやってみたいと思う。

エディタの設定

コードを書く前にエディタの設定をしておく。VSCodeの場合はIntegrating with VSCodeに習って、まずはVSCode SolidityをextensionとしてVSCodeにインストールした上で下記を.vscode/settings.jsonに書いて使う。

 {
   "solidity.packageDefaultDependenciesContractsDirectory": "src",
   "solidity.packageDefaultDependenciesDirectory": "lib",
   "solidity.compileUsingRemoteVersion": "v0.8.13"
 }

アプリの作成

 forge init foundry_playground

これで下記のようなレイアウトのプロジェクトが作成された。

 $ tree . -L 2
 ├── foundry.toml
 ├── lib
 │   └── forge-std
 ├── script
 │   └── Contract.s.sol
 ├── src
 │   └── Contract.sol
 └── test
     └── Contract.t.sol 

メインとなるコントラクトはsrc/に置き、そのテストはtest/に置く。script/にはデプロイ用のスクリプトを置き、lib/にはライブラリが置かれる。foundry.tomlはビルドやライブラリの依存関係のマッピングなどを書く設定ファイル。その他にはTest用のGitHub Actionsのワークフローがデフォルトで生成されていたりもした。気が利く。

ではビルドしてみる。エラーもなく成功するはず。

 forge build

テストしてみる。これもエラーなく成功するはず。

 forge test

ライブラリのインストール

OpenZeppelinをインストールする。これで.gitmoduleに追記が走るはず。実体はlib配下に置かれる。

 forge install openzeppelin/openzeppelin-contracts 

(補足)リマッピング

ライブラリをimportする時の呼び出し方をカスタムするためにremappingsという仕組みがある。例えばプロジェクトのルートにremappings.txtというファイルを置き、openzeppelin-utils=/=lib/openzeppelin-contracts/contracts/と書く。すると通常ならimport "openzeppelin-contracts/contracts/utils/Strings.sol"と書かなければいけないところをimport "openzeppelin-utils/utils/Strings.sol"と書けるようにできたりする。importの呼び出しパス名を短くできる。

File import callback not supported

forge install後にライブラリをimportした時にVSCode側でFile import callback not supportedというLintエラーが出ることがある。その場合はプロジェクトのルートにremappings.txtというファイルを置き、そこに下記を実行して出てきた情報をコピペすると治る。原因は分かってない。forge install openzeppelin/openzeppelin-contracts -hh-hhをつけてinstallするとうまくいくこともあるっぽい。

 $ forge remappings
 ds-test/=lib/forge-std/lib/ds-test/src/
 forge-std/=lib/forge-std/src/
 openzeppelin-contracts/=lib/openzeppelin-contracts/

実装

雑にmintとtokenURIのread用のメソッドを生やしたコントラクトを書く。

 // SPDX-License-Identifier: UNLICENSED
 pragma solidity ^0.8.13;
 
 import "openzeppelin-contracts/contracts/utils/Context.sol";
 import "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
 
 contract NFT is Context, ERC721Enumerable {
     using Strings for uint256;
 
     string baseURI = "";
 
     constructor(
         string memory name,
         string memory symbol,
         string memory uri
     ) ERC721(name, symbol) {
         require(bytes(uri).length > 0, "URI must be non-empty");
         baseURI = uri;
     }
 
     function mintTo(address to) public virtual {
         _mint(to, totalSupply());
     }
 
     function tokenURI(uint256 tokenId)
         public
         view
         virtual
         override
         returns (string memory)
     {
         return string(abi.encodePacked(baseURI, tokenId.toString(), ".json"));
     }
 }

importの呼び出しは通常のコントラクトだとimport "@openzeppelin-contracts/"みたいな感じだけど、foundryの場合はremappingsで設定されているpathを使って指定している。その他に特筆すべきことはない。

テスト

テストについてはWriting Tests - Foundry Bookを読むと良い。

NFT.solのテストも簡単に書いていく。

 // SPDX-License-Identifier: UNLICENSED
 pragma solidity ^0.8.13;
 
 import "forge-std/Test.sol";
 import "openzeppelin-contracts/contracts/utils/Strings.sol";
 import "../src/NFT.sol";
 
 contract NFTTest is Test {
     NFT public nftContract;
 
     function setUp() public {
         nftContract = new NFT("NFT", "NFT", "https://example.com/");
     }
     
     function testMintTo() public {
         address user1 = address(1);
         address user2 = address(2);
         nftContract.mintTo(user1);
         nftContract.mintTo(user2);
         uint256 tokenIndex1 = nftContract.tokenOfOwnerByIndex(user1, 0);
         uint256 tokenIndex2 = nftContract.tokenOfOwnerByIndex(user2, 0);
         assertEq(tokenIndex1, 0);
         assertEq(tokenIndex2, 1);
     }
 
     function testTokenURI(uint256 index) public {
         string memory result = nftContract.tokenURI(index);
         string memory expected = string(
             abi.encodePacked(
                 "https://example.com/",
                 Strings.toString(index),
                 ".json"
             )
         );
         assertEq(result, expected);
     }
 }

まずファイル名がNFT.t.solである。が、別に.tに意味は無く、NFT.solであってもtest/配下にあればforge testに対象になるのできにする必要なし。

import "forge-std/Test.sol";をimportしている部分。ここのファイルの中にマッチャーとか色々とテスト関連のライブラリが定義されている。これを継承してテストを書いていく。

setup()はテストごとに毎回実行される関数。無くても別にOK。

そのほかの関数がメインのテストケース。

testMintTo()の方は見たままの内容なので特に説明は不要だと思う。

testTokenURI(uint256 index)に関しては引数を取っている。これはfoundryの独自の仕組みであり便利な仕組みでもある。このようにテストケースに引数を取るようにするとファズテスト(境界値や異常値など様々な値で試すテスト)を行なってくれる。デフォルトでは256パターンのさまざまな値でテストを実行するようになる。

testTokenURIのケースではuint256の範囲でさまざまなindexでtokenURIを実行してくれてる。ただし今回はサンプルが悪くてありがたみがない...。Fuzz Testing - Foundry Bookのページのサンプルなどを読むとより理解できるはず。

デプロイ

Solidity Scriptingは名前の通りSolidity色々な実行処理などを書ける仕組み。今回はこの仕組みでデプロイスクリプトを書いていく。ちなみにSolidity ScriptingについてはSolidity Scripting - Foundry Bookを読むと良い。

Solidity Scripting用のファイルはscript/に置いておく。

 // SPDX-License-Identifier: UNLICENSED
 pragma solidity ^0.8.13;
 
 import "forge-std/Script.sol";
 import "../src/NFT.sol";
 
 contract NFTScript is Script {
     function setUp() public {}
 
     function run() public {
         vm.startBroadcast();
 
         NFT nftContract = new NFT("MyNFT", "NFT", "https://example.com/");
 
         vm.stopBroadcast();
     }
 }

import "forge-std/Script.sol";はスクリプト実行用のユーティリティ。contract NFTScript is Script {で継承して使う。

vm.startBroadcast()で以降のコントラクトの実行やトランザクション各種を記録しておき、vm.stopBroadcast()でそれらをネットワークに対してデプロイしていくという流れになる。

今回はローカルノードに対してデプロイしてみるのでanvilコマンドを実行する(これもfoundryのツール)。

 $ anvil
 Available Accounts
 ==================
 
 (0) 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
 (1) 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
 (2) 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH)
 ...
 
 Private Keys
 ==================
 
 (0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
 (1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
 (2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

anvilを実行するとHardhatと同じような感じでローカルノードが立ち上がるのでここにコントラクトをデプロイするだけ。デプロイは先ほどのsolidity scriptを実行する。

 forge script script/NFT.s.sol:NFTScript --fork-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast

デプロイが成功するとHashやかかったガス代やContract Addressなどが表示されるはず。

確認

デプロイされたコントラクトを実際に実行して確認してみる。

確認にはcastコマンドを使う。castはノードに対してRPCコールを行うCLIツール。先ほどデプロイしたコントラクトのアドレスが0x5fbdb2315678afecb367f032d93f642f64180aa3だとすると下記のように指定することでtokenURI関数を呼び出せる。

 cast call 0x5fbdb2315678afecb367f032d93f642f64180aa3 "tokenURI(uint256)(string memory)" 1 --rpc-url http://localhost:8545

https://example.com/1.jsonが返ってくるはず。このようにデプロイしたコントラクトをすぐにCLIで確認できるので便利。この他にもトランザクションを実行できたりインタラクティブモードもあったりと色々とできるのでOverview of Cast - Foundry Bookを読んでみると良い。

まとめ

FoundryというEthereumのコントラクト開発環境について書いた。

FoundryはHardhatと比べてSolidityだけで完結できるという点が嬉しい。Rustで書かれているからなのかビルドやテスト実行などが高速なのも良い。

まだまだ進化中なので毎日コミットが大量にされており変化も多そうだがTruffle/GanacheやHardhatの代替になり得るポテンシャルがあると感じた。

Rust Bookのような感じでFoundry Bookというドキュメントがあり、これを読むと完全に理解できる。興味が湧いたらぜひ読んで実際に手元で試してみることをおすすめする。

今回書いたコードは下記に置いておきます。
https://github.com/YuheiNakasaka/foundry_playground

リソース

Discussion

JJJJ

とても参考になる記事をありがとうございます。
1点、この記事をこの通りに実装してもうまくいかない点があったので修正リクエストを送らせていただきます。
デプロイ のコマンドが以下のように記事には記述されています

 forge script script/NFT.s.sol:NFTScript --fork-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast

しかしながら、作った deploy script の contract は記事の中では DeployScript となっています。そのため

Could not find target contract: <project root path>contract/script/NFT.s.sol:NFTScript

と言ったエラーが発生します。回避として

 forge script script/NFT.s.sol:DeployScript --fork-url http://localhost:8545 \
  --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcast

とすると通ります。ただ、GitHub のリポジトリを載せていただいていますがそちらのコードでは以下のように書かれているため
https://github.com/YuheiNakasaka/foundry_playground/blob/871c39f99868d2338a418dec1fc4946c8eae9c2d/script/NFT.s.sol#L7

記事の中で DeployScript と名前をつけた部分を NFTScript に修正するのがよさそうだと感じました。