Title
Weak Sources of Randomness from Chain Attributes
Relationships
CWE-330: Use of Insufficiently Random Values
Description
Ability to generate random numbers is very helpful in all kinds of applications. One obvious example is gambling DApps, where pseudo-random number generator is used to pick the winner. However, creating a strong enough source of randomness in Ethereum is very challenging. For example, use of block.timestamp
is insecure, as a miner can choose to provide any timestamp within a few seconds and still get his block accepted by others. Use of blockhash
, block.difficulty
and other fields is also insecure, as they're controlled by the miner. If the stakes are high, the miner can mine lots of blocks in a short time by renting hardware, pick the block that has required block hash for him to win, and drop all others.
Remediation
- Using external sources of randomness via oracles, and cryptographically checking the outcome of the oracle on-chain. e.g. Chainlink VRF. This approach does not rely on trusting the oracle, as a falsly generated random number will be rejected by the on-chain portion of the system.
- Using commitment scheme, e.g. RANDAO.
- Using external sources of randomness via oracles, e.g. Oraclize. Note that this approach requires trusting in oracle, thus it may be reasonable to use multiple oracles.
- Using Bitcoin block hashes, as they are more expensive to mine.
References
- How can I securely generate a random number in my smart contract?
- When can BLOCKHASH be safely used for a random number? When would it be unsafe?
- The Run smart contract
Samples
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_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);
}
}
}
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_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");
}
}
}
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;
}
}