EthereumのDapp開発環境のFoundryを使ってみる
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というドキュメントがあり、これを読むと完全に理解できる。興味が湧いたらぜひ読んで実際に手元で試してみることをおすすめする。
今回書いたコードは下記に置いておきます。
Discussion
とても参考になる記事をありがとうございます。
1点、この記事をこの通りに実装してもうまくいかない点があったので修正リクエストを送らせていただきます。
デプロイ のコマンドが以下のように記事には記述されています
しかしながら、作った deploy script の contract は記事の中では
DeployScript
となっています。そのためと言ったエラーが発生します。回避として
とすると通ります。ただ、GitHub のリポジトリを載せていただいていますがそちらのコードでは以下のように書かれているため
記事の中で
DeployScript
と名前をつけた部分をNFTScript
に修正するのがよさそうだと感じました。ありがとうございます。修正しておきます。