スマートコントラクト単体テストガイド──Hardhat・Foundry・Brownieで学ぶ堅牢なEthereum開発フロー

ブロックチェーン

はじめに

DeFiプロトコルやNFTプロジェクトが相次いでハッキング被害を受ける中、スマートコントラクトの単体テスト(Unit Testing)は「開発者の保険」ではなく必須条件になりました。オンチェーンにデプロイしたコードは基本的に変更不可能であり、1行のバグが数百万ドルを失わせる事例も珍しくありません。Ethereum.orgの公式ドキュメントは、Hardhat・Truffle・Foundry・Brownieなど主要フレームワークを用いたテスト戦略を紹介しています。本記事ではその内容を深掘りし、「何を・どうテストすればよいか」を体系的にまとめます。

単体テストの目的と3つの分類

機能検証(Functional Tests)

  • 関数の返値・状態変化が仕様どおりか
  • リバート条件が適切か(requirerevert

セキュリティ検証(Security Tests)

  • 再入可能性・整数オーバーフロー・アクセス制御漏れ
  • 想定外のキャッシュ汚染/ストレージ衝突

経済設計検証(Economic Tests)

  • 金利計算・報酬分配・トークン供給が意図どおりか
  • oracle操作やフラッシュローン攻撃耐性

Hardhatで学ぶJavaScript/TypeScriptテスト基礎

セットアップ

npm init -y
npm i --save-dev hardhat @nomiclabs/hardhat-ethers ethers chai
npx hardhat # 「JavaScriptプロジェクト」を選択

コントラクト(Lock.sol)

pragma solidity ^0.8.20;
contract Lock {
  uint public unlockTime;
  constructor(uint _unlockTime) payable {
    unlockTime = _unlockTime;
  }
  function withdraw() external {
    require(block.timestamp >= unlockTime, "locked");
    payable(msg.sender).transfer(address(this).balance);
  }
}

単体テスト(test/Lock.ts)

import { expect } from "chai";
import { ethers } from "hardhat";

describe("Lock", () => {
  it("ロック解除前はrevert", async () => {
    const [alice] = await ethers.getSigners();
    const Lock = await ethers.getContractFactory("Lock");
    const lock = await Lock.deploy((await ethers.provider.getBlock("latest")).timestamp + 60, { value: 1 });
    await expect(lock.withdraw()).to.be.revertedWith("locked");
  });

  it("ロック解除後に送金", async () => {
    const [alice] = await ethers.getSigners();
    const Lock = await ethers.getContractFactory("Lock");
    const unlockAt = (await ethers.provider.getBlock("latest")).timestamp + 60;
    const lock = await Lock.deploy(unlockAt, { value: 1 });
    await ethers.provider.send("evm_setNextBlockTimestamp", [unlockAt + 1]);
    await lock.withdraw();
    expect(await ethers.provider.getBalance(lock.address)).to.eq(0);
  });
});

ポイント

  • evm_setNextBlockTimestampでブロック時間を操作し時間依存ロジックをテスト
  • chaiexpect().to.be.revertedWith()でリバート理由まで検証

Foundryで高速Fuzz&Invariantテスト

インストール

curl -L https://foundry.paradigm.xyz | bash
foundryup
forge init foundry-demo && cd foundry-demo

Fuzzテスト例(Counter.t.sol)

pragma solidity ^0.8.23;
import "forge-std/Test.sol";

contract Counter {
  uint256 public num;
  function inc(uint256 x) external { num += x; }
}

contract CounterTest is Test {
  Counter c = new Counter();

  function testFuzz_Inc(uint256 x) public {
    vm.assume(x < 1e18);
    uint256 before = c.num();
    c.inc(x);
    assertEq(c.num(), before + x);
  }
}
  • forge test -vvvvは100件以上のランダム入力で検証
  • vm.assumeで現実的な入力に制限し不要なガス浪費を抑制

Invariantテスト

contract Invariant is Test {
  Counter c = new Counter();

  function invariant_sumNotOverflow() public {
    assertLe(c.num(), type(uint256).max / 2);
  }
}

Foundryは状態を保存しながら数千回ランダムシーケンスを実行、インバリアント破壊ケースを自動検出します。

Brownie(Python)でのテスト&カバレッジ

インストール

pip install eth-brownie
brownie init

PyTest記法

import pytest
def test_withdraw(accounts, Lock):
    alice = accounts[0]
    lock = Lock.deploy(0, {'from': alice, 'value': '1 ether'})
    with pytest.reverts("locked"):
        lock.withdraw({'from': alice})

カバレッジ測定

brownie test --coverage

HTMLレポートで行単位の未テスト箇所を可視化。Solidityファイルが赤色ならテスト追加が必要です。

モック & スタブ技法

シナリオ手法フレームワーク
Oracle価格を固定MockAggregatorV3をデプロイHardhat/Foundry
ERC20転送成功/失敗FakeTokenでtransferを戻り値操作Hardhat
Chainlink VRFvm.rollで乱数固定Foundry

外部コントラクトをモック化することで、単体テストは純粋関数的になり失敗箇所特定が迅速になります。

CI/CD統合ベストプラクティス

  1. GitHub Actionsforge testnpx hardhat testを実行
  2. カバレッジ閾値(例:90%)未満ならPRブロック
  3. actions/cacheでFoundryキャッシュ・node_modulesを高速化
  4. Solidity静的解析(Slither)・Lint(Solhint)を並列ジョブで追加
  5. テスト通過後のみhardhat-deployまたはforge scriptでTestnetへ自動デプロイ

よくある落とし穴と対策

問題症状対策
ForkテストのRPC制限Alchemy/EtherscanエラーALCHEMY_MAX_CALL=30に増枠プラン・--fork-block-number固定
フラッシュローン攻撃検出漏れテストが単一TxHardhat NetworkでautoMine=falseにし複数呼び出し
時間依存テストの不安定CIで偶発failブロックタイムを固定&evm_increaseTime instead of sleep

まとめ

スマートコントラクトの単体テストは機能・セキュリティ・経済設計の3軸を網羅する必要があります。Remixで概念を掴み、HardhatやFoundryで高速なローカルテストを構築し、BrownieやPythonの資産を活かして統計解析を行う――この多段構成が2025年現在の“最強パターン”です。
CIにテストと静的解析を組み込み、カバレッジとFuzzingで抜け漏れを潰せば、メインネットデプロイ後に重大バグが見つかるリスクを大幅に下げられます。この記事を参考に、まずはforge initnpx hardhatを実行し、あなたのコントラクトを徹底的に叩き上げてみてください。

コメント