严重随机数静态扫描有 1 条规则
可预测的随机数
Weak Randomness
链上没有真正的随机数。用 block.timestamp / blockhash / msg.sender 等链上可见的值当随机源,等于把开奖结果提前公开——矿工(或任何能预测的人)都能动手脚。
根因
EVM 的确定性要求:所有节点必须对同一笔交易算出一模一样的结果,因此链上不存在「只有你不知道」的值。用链上变量当种子,攻击者可以在同一个交易内复算出结果并决定要不要提交。
攻击是怎么一步步发生的
- 1攻击者合约
在交易里先本地复算 seed 与 result
- 2攻击者合约
若 result < 10(中奖)则继续调用 draw(),否则直接 revert
- 3结果
攻击者只在必胜时出手,抽奖变成「确定收益」,奖池被逐步搬空
有漏洞的版本
不要照抄soliditySolidity16 行
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)
修复版本
推荐写法soliditySolidity27 行
// 方案 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但「命中规则」只说明代码里出现了相关的写法,不说明它一定有问题、也不说明没有它就没问题。命中了就回到源码确认调用路径。