Solidity を書き始めた Web 開発者が最初につまずくのは、言語の文法ではありません。ツールチェーンの世界観です。
対応表
Foundry は 4 つのコマンドからなります。
| Foundry | Web 開発での相当物 |
|---|---|
forge build | tsc / vite build |
forge test | jest / vitest。ただしテストも Solidity で書く |
forge fmt | prettier |
anvil | docker compose up postgres。使い捨てのチェーンが立つ |
cast | curl / httpie。チェーンに読み書きする CLI |
chisel | node の REPL。Solidity の式をその場で試せる |
foundry.toml | package.json + tsconfig.json |
lib/ + git submodule | node_modules + npm |
最後の行が最大の驚きポイントです。順に見ていきます。
インストール
curl -L https://foundry.paradigm.xyz | bash
foundryup
Homebrew でも入ります(brew install foundry)。forge --version が出れば完了です。
なぜ npm ではなく git submodule なのか
Foundry の依存管理はこうなっています。
forge install foundry-rs/forge-std
これは npm レジストリではなく、GitHub のリポジトリを git submodule として lib/ に追加します。バージョンは npm の semver ではなく、git のタグやコミットハッシュで固定されます。
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
奇妙に見えますが、理由があります。
Solidity には中央レジストリの文化が定着しませんでした。 OpenZeppelin のようなデファクト標準ライブラリは npm でも配布されていますが、それ以外の多くは GitHub 上のソースが正典です。そしてこの領域では「バージョン ^4.9.0 が指す実体が後から変わる」ことが致命的です。監査したコードと、デプロイされたコードが 1 バイトも違ってはいけない。commit ハッシュで固定するのは、その要求への素直な答えでした。
2024 年以降は Soldeer という Foundry 公式のパッケージマネージャも使えますが、submodule 方式が依然として主流です。
実務上の注意: submodule なので git clone しただけでは lib/ が空になります。
git submodule update --init --recursive
これを README に書き忘れると、他人の環境で必ずビルドが落ちます。
なぜテストを Solidity で書くのか
これも最初は面食らいます。Hardhat が TypeScript でテストを書くのに対し、Foundry は Solidity で書きます。
function test_XWinsTheTopRow() public {
vm.prank(alice);
game.play(0);
vm.prank(bob);
game.play(3);
// ...
assertEq(game.winner(), alice);
}
理由は 2 つあります。
1. 速い。 テストが EVM の中で完結するので、JavaScript と EVM の間で RPC を往復しません。数百のテストが数十ミリ秒で終わります。本書のコントラクトは 67 テストが 0.1 秒未満で完走します。この速さは、テストを書く量そのものを変えます。
2. 型と単位が同じ。 uint256 を JavaScript の BigInt に変換して比較する、という作業が要りません。address も bytes32 もそのまま扱えます。コントラクトの内部関数を直接呼ぶこともできます。
代償として、テストコードに JavaScript のエコシステム(モックライブラリ、スナップショットテスト等)は使えません。しかしスマートコントラクトのテストで本当に必要なのは、外部サービスのモックではなく状態遷移の検証なので、あまり困りません。
vm — テスト専用の魔法
上のテストに出てきた vm.prank(alice) が Foundry の中核です。チートコードと呼ばれる、テスト中だけ使える EVM への介入手段です。
| チートコード | やること |
|---|---|
vm.prank(addr) | 次の呼び出しの msg.sender を addr にする |
vm.deal(addr, amount) | アドレスに残高を配る |
vm.warp(timestamp) | ブロックのタイムスタンプを進める |
vm.roll(blockNumber) | ブロック番号を進める |
vm.expectRevert(...) | 次の呼び出しが特定のエラーで失敗することを期待する |
vm.expectEmit(...) | 特定のイベントが発火することを期待する |
vm.warp は特に重要です。本書のコントラクトには「1 時間経ったら不戦勝」という機能が入りますが、テストで 1 時間待つわけにはいきません。vm.warp(block.timestamp + 1 hours + 1) で時間を飛ばします。
fuzz テストが標準装備
Foundry のテスト関数が引数を取ると、自動的に fuzz テストになります。
function testFuzz_PotConservation(uint96 stake, uint16 feeBps) public {
stake = uint96(bound(stake, 0, 10 ether));
feeBps = uint16(bound(feeBps, 0, game.MAX_FEE_BPS()));
// ...
assertEq(game.pending(alice) + game.pending(owner), pot);
}
Foundry がランダムな入力を(既定で 256 回、設定で変更可)投げ込みます。JavaScript でいう fast-check が最初から入っている、と思ってください。金額や境界値を扱うコードでは、手で書いた数例より遥かに信頼できます。
失敗するとその入力値が表示されるので、再現テストに落とせます。
anvil — 使い捨てのブロックチェーン
anvil
これだけで、ローカルに Ethereum が立ちます。
- ポート 8545、chain ID 31337
- 10,000 ETH 入りのアカウントが 10 個(決定論的に生成、毎回同じ)
- トランザクションを送ると即座にブロックが作られる
- ディスクには何も書かない
最後の点が重要です。プロセスを落とすとチェーンごと消えます。docker compose down -v で消える使い捨ての Postgres と同じ立ち位置です。壊し放題なので、実験場としてこれ以上のものはありません。
デプロイしたコントラクトのアドレスも決定論的です。同じ鍵で 1 番目にデプロイすれば、必ず 0x5FbDB2315678afecb367f032d93F642f64180aa3 になります。ただしこれは裏を返すと、既存のチェーンを再利用して 2 回デプロイすると nonce がずれてアドレスが変わるということでもあります。自動化スクリプトを書くときの定番の落とし穴です。
cast — チェーンに対する curl
cast block-number --rpc-url http://127.0.0.1:8545
cast call 0x5FbD... "number()(uint256)"
cast send 0x5FbD... "setNumber(uint256)" 42 --private-key 0x...
cast storage 0x5FbD... 0
cast は JSON-RPC のラッパーです。読み書きだけでなく、エンコード・デコードの道具箱としても使います。
cast sig "play(uint256,uint8)" # 関数セレクタを計算
cast 4byte 0x3fb5c1cb # セレクタから関数名を逆引き
cast --to-wei 0.1 ether # 単位変換(wei = ETH の最小単位、1 ETH = 10^18 wei)
cast disassemble <bytecode> # EVM のバイトコードを逆アセンブル
次章では、この cast を使って最小のコントラクトを 8 通りに覗きます。コントラクトが実際には何なのかを、一番手っ取り早く理解できる方法だと思っています。
プロジェクトを作る
forge init myproject
cd myproject
forge test
生成される構成はこうです。
foundry.toml 設定
src/ コントラクト本体(手書き)
test/ テスト(手書き、Solidity)
script/ デプロイスクリプト(手書き、Solidity)
lib/ 依存(submodule。触らない)
out/ ビルド成果物(触らない)
cache/ ビルドキャッシュ(触らない)
broadcast/ デプロイ実行の記録(触らない)
out/ cache/ broadcast/ は .gitignore に入れます。lib/ は submodule として .gitmodules で管理されるので、中身はコミットしません。
準備は以上です。次章から実際に動かします。