「テストコードを書けば安全」という幻想を捨てろ:形式検証(Formal Verification)がWeb3の現場で不可欠な理由
現場で日々コントラクトを叩いていると、たまに「ユニットテストは100%通っているから大丈夫」なんて楽観的な言葉を耳にする。ハッキリ言おう。それは、「鍵をかけたから泥棒は入らない」と言っているのと同じだ。
Web3の脆弱性は、論理の穴を突く。どれだけカバレッジ100%のテストを書こうが、開発者が「想定していなかった状態遷移」はテストケースには現れない。そこで登場するのが、CertoraやK-Frameworkに代表される「形式検証(Formal Verification)」だ。
1. なぜ「テスト」では不十分なのか?
一般的なユニットテストは「入力値 A を入れたら出力値 B が返る」という期待値検証だ。しかし、DeFiのハッキングの多くは、Flash Loan(フラッシュローン)を悪用した複雑な再帰呼び出しや、複数のコントラクトを跨いだ「状態の不整合」によって引き起こされる。
攻撃者は、開発者がテストコードで考慮しなかった「極端な状態(Edge Cases)」を突いてくる。形式検証は、数学的証明を用いて「コントラクトの全状態において、仕様が破られることがないか」を網羅的にチェックする。これは人間が書くテストとは次元が違う、論理の「防波堤」だ。
2. 形式検証を意識した設計:不変条件(Invariants)の定義
形式検証の第一歩は、コントラクトの「不変条件」を定義することにある。例えば、トークンの総供給量(totalSupply)と、各ユーザーの残高の合計(balanceOf)は常に一致しなければならない。これが崩れた瞬間、それはバグではなく「ハッキング」だ。
以下の Solidity のコード例を見てほしい。ここでは、状態の変化を数学的に記述するための考え方を示している。
// セキュアなコントラクト設計の基本:不変条件の明示
contract Vault {
mapping(address => uint256) public balances;
uint256 public totalAssets;
// 形式検証において最も重要な「不変条件(Invariant)」
// このコントラクトの状態遷移において、常にこの関数が true を返す必要がある
function checkInvariant() public view returns (bool) {
uint256 sumOfBalances = 0;
// 実際にはループ処理はコスト高なので検証ツール側で計算させる
// ここでは論理的な整合性を保つための設計指針を示す
return sumOfBalances <= totalAssets;
}
function deposit(uint256 amount) external {
balances[msg.sender] += amount;
totalAssets += amount;
// 状態変更の直後に不変条件をチェックする(実務ではmodifierで実装)
require(checkInvariant(), "Invariant violation!");
}
}
3. Certora による検証の自動化(概念)
Certoraのようなツールを使うと、上記の checkInvariant を数学的に証明できる。開発者は「ルールファイル(.cvl)」を作成し、どのような状態であっても totalAssets が改ざんされないことをツールに教え込む。
// Certora用ルールファイルの例 (Rule.cvl)
rule total_assets_must_be_consistent {
env e;
uint256 amount;
// 任意の deposit 呼び出しの後で...
deposit(e, amount);
// 不変条件が満たされていることを数学的に証明する
assert(checkInvariant());
}
このコードを流せば、人間が何万回テストを回しても見つけられない「攻撃の入り口」を、ツールが数学的に弾き出してくれる。
4. 現場で今すぐやるべき「泥臭い」守り方
形式検証は強力だが、導入コストも高い。だからこそ、日々の開発で以下の「ガードレール」を徹底してほしい。
A. Reentrancy Guard の徹底
再入可能攻撃はWeb3の基本にして最悪のバグだ。OpenZeppelinの ReentrancyGuard は絶対にそのまま使え。
// JavaScript/Hardhat環境でのテストケース例
// 再入攻撃をテストコードでシミュレートする際の一歩
it("should revert if attacker tries to re-enter", async () => {
await attacker.attack();
// 状態がロールバックされていることを確認
expect(await vault.balanceOf(attacker.address)).to.equal(0);
});
B. Nginx / WAF でのメタデータ保護
Web3のコントラクトそのものはブロックチェーン上だが、フロントエンド(DApp)はWebアプリだ。RPCエンドポイントを直接叩かれないよう、Nginx側でアクセス制御を行うのは基本中の基本だ。
# Nginx設定ファイル例
location /api/rpc {
# 悪意のあるbotからの大量リクエストを制限
limit_req zone=rpc_limit burst=5 nodelay;
# 特定のオリジン以外からのアクセスを拒否
add_header Access-Control-Allow-Origin "https://your-dapp.com";
# Cloudflare等のWAFでブロックできない異常通信をここで弾く
if ($http_user_agent ~* (sqlmap|nikto|nmap)) {
return 403;
}
}
最後に:セキュリティは「終わりのないマラソン」だ
形式検証を導入したからといって、100%安全になるわけではない。仕様書そのものが間違っていれば、ツールは「仕様通りに間違っていること」を証明してくれるだけだからだ。
大切なのは、「自分のコードは常に脆弱である」という謙虚な前提に立ち、数学的な証明と、泥臭い境界値テストを組み合わせること。
後輩諸君、ツールに頼り切るのではなく、ツールを使って「自分の論理の甘さ」を暴く快感を覚えてほしい。それができるエンジニアだけが、Web3の荒波を生き残れる。コードを書き終えたら、次は「このコードをどうやって破壊するか」を考える、その視点を忘れないように。
コメント