SWC-120/基于链属性的弱随机性
生成随机数的能力在各种应用中都非常有用。一个明显的例子是博彩DApp,其中使用伪随机数 生成器选择获胜者。但是,在以太坊中创建足够强大的随机性来源非常具有挑战性。例如, 使用block.timestamp是不安全的,因为矿工可以选择在几秒钟内提供任何时间戳,而仍然使 他的区块被其他人接受。使用blockhash、block.difficulty以及其他字段也是不安全的,因为 它们是由矿工控制。如果下注很高,那么该矿工可以在短时间内通过租用硬件来挖掘很多区块, 选择所需的区块哈希值,然后放弃所有其他区块。
CWE漏洞分类
整改方案
- 使用commitment方案,例如RANDAO。
- 通过预言机/oracle使用外部随机性源,例如Oraclize。请注意,此方法需要信任oracle, 因此使用多个oracle是合理的。
- 使用比特币区块哈希,因为它们的开采成本更高。
参考文献
合约示例
guess_the_random_number.sol
/*
* @source: https://capturetheether.com/challenges/lotteries/guess-the-random-number/
* @author: Steve Marx
*/
pragma solidity ^0.4.21;
contract GuessTheRandomNumberChallenge {
uint8 answer;
function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
guess_the_random_number.yaml
description: Guess the number from a predictable on chain source
issues:
- id: SWC-120
count: 1
locations:
- bytecode_offsets: {}
line_numbers:
guess_the_random_number.sol: [14]
guess_the_random_number_fixed.sol
/*
* @source: https://capturetheether.com/challenges/lotteries/guess-the-random-number/
* @author: Steve Marx
*/
pragma solidity ^0.4.25;
contract GuessTheRandomNumberChallenge {
uint8 answer;
uint8 commitedGuess;
uint commitBlock;
address guesser;
function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
//Guess the modulo of the blockhash 20 blocks from your guess
function guess(uint8 _guess) public payable {
require(msg.value == 1 ether);
commitedGuess = _guess;
commitBlock = block.number;
guesser = msg.sender;
}
function recover() public {
//This must be called after the guessed block and before commitBlock+20's blockhash is unrecoverable
require(block.number > commitBlock + 20 && commitBlock+20 > block.number - 256);
require(guesser == msg.sender);
if(uint(blockhash(commitBlock+20)) == commitedGuess){
msg.sender.transfer(2 ether);
}
}
}
guess_the_random_number_fixed.yaml
description: Guess the number from a predictable on chain source
issues:
- id: SWC-120
count: 0
locations: []
old_blockhash.sol
pragma solidity ^0.4.24;
//Based on the the Capture the Ether challange at https://capturetheether.com/challenges/lotteries/predict-the-block-hash/
//Note that while it seems to have a 1/2^256 chance you guess the right hash, actually blockhash returns zero for blocks numbers that are more than 256 blocks ago so you can guess zero and wait.
contract PredictTheBlockHashChallenge {
struct guess{
uint block;
bytes32 guess;
}
mapping(address => guess) guesses;
constructor() public payable {
require(msg.value == 1 ether);
}
function lockInGuess(bytes32 hash) public payable {
require(guesses[msg.sender].block == 0);
require(msg.value == 1 ether);
guesses[msg.sender].guess = hash;
guesses[msg.sender].block = block.number + 1;
}
function settle() public {
require(block.number > guesses[msg.sender].block);
bytes32 answer = blockhash(guesses[msg.sender].block);
guesses[msg.sender].block = 0;
if (guesses[msg.sender].guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
old_blockhash.yaml
description: Weak Sources of Randomness
issues:
- id: SWC-120
count: 1
locations:
- bytecode_offsets:
'0x80e6291d79fe0fafbb9c90d16e87a28fc8666591fb40a875ec974b95462002d1': [442]
line_numbers:
old_blockhash.sol: [29,33]
old_blockhash_fixed.sol
pragma solidity ^0.4.24;
//Based on the the Capture the Ether challange at https://capturetheether.com/challenges/lotteries/predict-the-block-hash/
//Note that while it seems to have a 1/2^256 chance you guess the right hash, actually blockhash returns zero for blocks numbers that are more than 256 blocks ago so you can guess zero and wait.
contract PredictTheBlockHashChallenge {
struct guess{
uint block;
bytes32 guess;
}
mapping(address => guess) guesses;
constructor() public payable {
require(msg.value == 1 ether);
}
function lockInGuess(bytes32 hash) public payable {
require(guesses[msg.sender].block == 0);
require(msg.value == 1 ether);
guesses[msg.sender].guess = hash;
guesses[msg.sender].block = block.number + 1;
}
function settle() public {
require(block.number > guesses[msg.sender].block +10);
//Note that this solution prevents the attack where blockhash(guesses[msg.sender].block) is zero
//Also we add ten block cooldown period so that a minner cannot use foreknowlege of next blockhashes
if(guesses[msg.sender].block - block.number < 256){
bytes32 answer = blockhash(guesses[msg.sender].block);
guesses[msg.sender].block = 0;
if (guesses[msg.sender].guess == answer) {
msg.sender.transfer(2 ether);
}
}
else{
revert("Sorry your lottery ticket has expired");
}
}
}
old_blockhash_fixed.yaml
description: Weak Sources of Randomness
issues:
- id: SWC-120
count: 0
locations: []
random_number_generator.sol
pragma solidity ^0.4.25;
// Based on TheRun contract deployed at 0xcac337492149bDB66b088bf5914beDfBf78cCC18.
contract RandomNumberGenerator {
uint256 private salt = block.timestamp;
function random(uint max) view private returns (uint256 result) {
// Get the best seed for randomness
uint256 x = salt * 100 / max;
uint256 y = salt * block.number / (salt % 5);
uint256 seed = block.number / 3 + (salt % 300) + y;
uint256 h = uint256(blockhash(seed));
// Random number between 1 and max
return uint256((h / x)) % max + 1;
}
}
random_number_generator.yaml
description: Weak Sources of Randomness
issues:
- id: SWC-120
count: 4
locations:
- bytecode_offsets: {}
line_numbers:
random_number_generator.sol: [5]
- bytecode_offsets: {}
line_numbers:
random_number_generator.sol: [10]
- bytecode_offsets: {}
line_numbers:
random_number_generator.sol: [11]
- bytecode_offsets: {}
line_numbers:
random_number_generator.sol: [12]