Solidity を書き始めた Web 開発者が最初につまずくのは、言語の文法ではありません。ツールチェーンの世界観です。

対応表

Foundry は 4 つのコマンドからなります。

FoundryWeb 開発での相当物
forge buildtsc / vite build
forge testjest / vitest。ただしテストも Solidity で書く
forge fmtprettier
anvildocker compose up postgres使い捨てのチェーンが立つ
castcurl / httpie。チェーンに読み書きする CLI
chiselnode の REPL。Solidity の式をその場で試せる
foundry.tomlpackage.json + tsconfig.json
lib/ + git submodulenode_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 に変換して比較する、という作業が要りません。addressbytes32 もそのまま扱えます。コントラクトの内部関数を直接呼ぶこともできます。

代償として、テストコードに JavaScript のエコシステム(モックライブラリ、スナップショットテスト等)は使えません。しかしスマートコントラクトのテストで本当に必要なのは、外部サービスのモックではなく状態遷移の検証なので、あまり困りません。

vm — テスト専用の魔法

上のテストに出てきた vm.prank(alice) が Foundry の中核です。チートコードと呼ばれる、テスト中だけ使える EVM への介入手段です。

チートコードやること
vm.prank(addr)次の呼び出しの msg.senderaddr にする
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 で管理されるので、中身はコミットしません。

準備は以上です。次章から実際に動かします。