W3
严重随机数静态扫描有 1 条规则

可预测的随机数

Weak Randomness

链上没有真正的随机数。用 block.timestamp / blockhash / msg.sender 等链上可见的值当随机源,等于把开奖结果提前公开——矿工(或任何能预测的人)都能动手脚。

根因

EVM 的确定性要求:所有节点必须对同一笔交易算出一模一样的结果,因此链上不存在「只有你不知道」的值。用链上变量当种子,攻击者可以在同一个交易内复算出结果并决定要不要提交。

攻击是怎么一步步发生的

  1. 1
    攻击者合约

    在交易里先本地复算 seed 与 result

  2. 2
    攻击者合约

    若 result < 10(中奖)则继续调用 draw(),否则直接 revert

  3. 3
    结果

    攻击者只在必胜时出手,抽奖变成「确定收益」,奖池被逐步搬空

有漏洞的版本

不要照抄solidity
Solidity16
contract WeakLottery {
    // 用链上可见值拼随机数
    function draw() external payable returns (uint256) {
        require(msg.value == 0.01 ether, "wrong stake");

        uint256 seed = uint256(
            keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))
        );
        uint256 result = seed % 100;      // 0–99 之间「随机」

        if (result < 10) {
            payable(msg.sender).transfer(address(this).balance);  // 10% 中奖
        }
        return result;
    }
}
  • block.timestamp / block.prevrandao / msg.sender 在交易执行时对所有人都是已知或可预测的
  • 攻击者写一个合约:在自己的交易里先算出 result,只有中奖才继续 —— 抽不到就 revert,只损失 gas
  • 验证者还能通过不出块、或在多个候选块里挑一个有利的来进一步操纵(prevrandao)

修复版本

推荐写法solidity
Solidity27
// 方案 A:承诺-揭示(Commit-Reveal)—— 适合无对手方的抽奖
contract CommitRevealLottery {
    mapping(address => bytes32) public commits;
    mapping(address => uint256) public commitBlock;

    function commit(bytes32 h) external {
        commits[msg.sender] = h;
        commitBlock[msg.sender] = block.number;
    }

    function reveal(uint256 secret) external {
        require(commits[msg.sender] == keccak256(abi.encodePacked(secret, msg.sender)), "bad reveal");
        // 要求至少过了若干区块,确保提交时无法知道后续的种子
        require(block.number > commitBlock[msg.sender] + 5, "too early");

        // 种子在提交时还不存在,提交者无法提前算好中奖条件
        bytes32 seed = keccak256(abi.encodePacked(secret, blockhash(commitBlock[msg.sender] + 5)));
        uint256 result = uint256(seed) % 100;
        require(result < 10, "not a winner");
        commits[msg.sender] = bytes32(0);
        // ...发奖
    }
}

// 方案 B(生产环境首选):Chainlink VRF
// 随机数由预言机链下生成,附带回链上验证的证明,
// 合约只负责验证证明,无法被矿工或调用者预测。
  • Commit-Reveal 的关键是「提交时种子还不存在」,先把选择钉死,再揭晓结果
  • 单个用户的抽奖用 Commit-Reveal 就够;涉及大额奖池或对手方博弈,用 Chainlink VRF
  • 不要用 blockhash(block.number - 1) —— 同一区块内它是固定的,攻击者可复现
  • blockhash 只能取最近 256 个区块,更早的会返回 0,这也是常见 bug 来源

怎么防

  • 链上不要自己造随机数:用 Chainlink VRF 这类可验证随机函数
  • 退一步用 Commit-Reveal,并把「提交」与「揭示」间隔若干区块
  • 不要用 block.timestamp / blockhash / block.prevrandao / msg.sender 单独或组合做种子
  • 如果随机结果涉及资金,必须考虑「验证者能否选择性不出块」

真实案例

2019 起多款「链上彩票 / 转盘」类合约单起数万至数十万美元

这类漏洞至今仍在批量复现,是新手项目最常见的失陷点之一

2022多个 NFT 盲盒铸造稀有度被刷走

用区块变量决定稀有度,被脚本批量筛选最优区块

审计这个合约时要问的问题

  • 1.随机数种子来自哪里?攻击者能不能在提交交易前算出结果?
  • 2.攻击者能否通过 revert 来「只在中奖时花费 gas」?
  • 3.奖励金额是否高到值得验证者去操纵区块变量?

这些问题也是审计检查清单里对应的条目——那里可以把每一条的检查结果记下来,最后导出成报告。

静态扫描能发现它吗

能——扫描引擎里有 1 条规则与它相关:

weak-randomness

但「命中规则」只说明代码里出现了相关的写法,不说明它一定有问题、也不说明没有它就没问题。命中了就回到源码确认调用路径。

延伸阅读

下一步:想知道这条在手上的合约里有没有出现,用Rug Pull 扫描器,它会检查前面列出的 1 条相关规则。 审计检查清单可以把它落成一条可勾选、可写备注的记录,最后导出 Markdown 报告;想先弄懂这一类漏洞背后的通用思路,去参考手册看对应的语言与标准条目。