How to Cheat Crypto Casinos: Predicting Random Numbers in Ethereum Smart Contracts
Ethereum has gained massive popularity as a platform for ICOs, but its use goes far beyond creating ERC-20 tokens. The Ethereum blockchain can also be used for online roulette, lotteries, and card games. While blockchain-confirmed transactions are tamper-proof due to decentralization and transparency, smart contract code can still be vulnerable. One major issue is insecure random number generators (RNGs). Let’s break down common RNG implementation mistakes in Ethereum-based gambling games.
Why Randomness Is Hard in Ethereum
Ethereum allows the execution of Turing-complete programs, usually written in Solidity, which is why its founders call it a “world computer.” Its transparency makes it attractive for online gambling, where user trust is crucial.
However, Ethereum’s blockchain processes are predictable, making it difficult to write a secure RNG—an essential part of any gambling game. We analyzed smart contracts to assess the reliability of Solidity-based RNGs and identify common design flaws that allow attackers to predict random numbers.
INFO
In 2017, Positive Technologies experts analyzed the security of ICO procedures and blockchain implementations. The results were grim: vulnerabilities were found in 71% of projects, and 23% had flaws that allowed attacks on investors. On average, each ICO project contained five vulnerabilities. But attackers only need one to steal investors’ money.
Our research included these steps:
- Collected 3,649 smart contracts from etherscan.io and GitHub.
- Imported contracts into Elasticsearch, an open-source search engine.
- Used Kibana’s web interface to find 72 unique RNG implementations.
- Manually analyzed each contract and found 43 were vulnerable.
Vulnerable RNGs
Our analysis revealed four categories of vulnerable RNGs:
- Generators using block variables as a source of entropy
- Generators using the previous block’s hash
- Generators using the previous block’s hash combined with a supposedly secret seed
- Generators vulnerable to front-running attacks
Let’s look at each category with code examples.
Generators Using Block Variables
Several block variables are mistakenly used as entropy sources:
- block.coinbase: the miner’s address for the current block
- block.difficulty: the difficulty of mining the block
- block.gaslimit: the gas limit for all transactions in the block
- block.number: the block’s height
- block.timestamp: the block’s mining timestamp
Miners can manipulate all these variables, so they’re unsuitable as entropy sources. Also, these variables are identical within a block. If an attacker’s contract calls a victim’s contract internally, both will get the same RNG value.
Example 1:
// Won if block number is even
// (note: this is a terrible source of randomness, please don’t use this with real money)
bool won = (block.number % 2) == 0;
Example 2:
// Compute some *almost random* value for selecting winner from current transaction.
var random = uint(sha3(block.timestamp)) % 2;
Example 3:
address seed1 = contestants[uint(block.coinbase) % totalTickets].addr;
address seed2 = contestants[uint(msg.sender) % totalTickets].addr;
uint seed3 = block.difficulty;
bytes32 randHash = keccak256(seed1, seed2, seed3);
uint winningNumber = uint(randHash) % totalTickets;
address winningAddress = contestants[winningNumber].addr;
Generators Using Block Hash
Each Ethereum block has a hash for transaction verification. The Ethereum Virtual Machine (EVM) provides the block.blockhash()
function, which takes a block number as an argument. We found that block.blockhash()
is often misused for randomness.
There are three main vulnerable variations:
block.blockhash(block.number)
: hash of the current blockblock.blockhash(block.number-1)
: hash of the previous blockblock.blockhash()
: hash of a block at least 256 blocks old
block.blockhash(block.number)
At the time a transaction is executed, the hash of the current block is unknown, so EVM always returns zero. Some contracts mistakenly use this as a source of entropy.
Example 1:
function deal(address player, uint8 cardNumber) internal returns (uint8) {
uint b = block.number;
uint timestamp = block.timestamp;
return uint8(uint256(keccak256(block.blockhash(b), player, cardNumber, timestamp)) % 52);
}
Example 2:
function random(uint64 upper) public returns (uint64 randomNumber) {
_seed = uint64(sha3(sha3(block.blockhash(block.number), _seed), now));
return _seed % upper;
}
block.blockhash(block.number-1)
Some contracts use the previous block’s hash. This is also vulnerable: an attacker can create an exploit contract with the same RNG value and call the target contract internally, resulting in matching “random” numbers.
Example:
//Generate random number between 0 & max
uint256 constant private FACTOR = 1157920892373161954235709850086879078532699846656405640394575840079131296399;
function rand(uint max) constant private returns (uint256 result){
uint256 factor = FACTOR * 100 / max;
uint256 lastBlockNumber = block.number - 1;
uint256 hashVal = uint256(block.blockhash(lastBlockNumber));
return uint256((uint256(hashVal) / factor)) % max;
}
block.blockhash()—Hash of a Future Block
A more secure method is to use the hash of a future block:
- The player places a bet; the casino records the transaction’s
block.number
. - On a second call, the player requests the winning number.
- The casino retrieves the saved
block.number
, gets the block hash, and uses it for RNG.
This only works if the second call happens within 256 blocks; otherwise, the hash is zero. This vulnerability was exploited in the SmartBillions lottery hack, where a player won 400 ETH by waiting 256 blocks and then requesting a predictable winning number.
Generators Using Previous Block Hash with a Secret Seed
Some contracts try to increase entropy by using a secret seed, as in the Slotthereum lottery:
bytes32 _a = block.blockhash(block.number - pointer)
for (uint i = 31; i >= 1; i--) {
if ((uint8(_a[i]) >= 48) && (uint8(_a[i]) <= 57)) {
return uint8(_a[i]) - 48;
}
}
The pointer
variable is private, so other contracts can’t access it. However, blockchain transparency means private variables can be extracted from contract storage using tools like web3.eth.getStorageAt()
. Attackers can retrieve pointer
and use it in an exploit:
function attack(address a, uint8 n) payable {
Slotthereum target = Slotthereum(a);
pointer = n;
uint8 win = getNumber(getBlockHash(pointer));
target.placeBet.value(msg.value)(win, win);
}
Generators Vulnerable to Front-Running
Miners can maximize rewards by ordering transactions based on gas price; the highest gas price gets executed first. If contract execution depends on transaction order, this can be exploited.
For example, a lottery uses an external “oracle” to get random numbers. Attackers can monitor the pending transaction pool and, when the oracle’s number appears, submit a bet with a higher gas price, ensuring their transaction is executed first and winning the round. This technique was demonstrated at the ZeroNights ICO Hacking Contest.
Another example is the “Last is me!” game, where the last ticket buyer wins if no one else buys a ticket within a set number of blocks. Attackers can monitor pending transactions and outbid others with a higher gas price to win.
How to Create a More Secure RNG
There are several approaches to building a more secure RNG in Ethereum:
- External oracle
- Signidice algorithm
- Commit–Reveal approach
External Oracles
Oraclize
Oraclize is a service for decentralized apps that connects the blockchain to the outside world (the internet). Smart contracts can request data from web APIs (like currency rates or weather) and even get random numbers from random.org.
The main drawback is centralization. Can we trust Oraclize or random.org? Oraclize uses TLSNotary for verification, but this happens off-chain. It’s better to use Oraclize with Ledger proofs, which can be verified on-chain.
BTC Relay
BTC Relay bridges Ethereum and Bitcoin. Ethereum contracts can request future Bitcoin block hashes as entropy. For example, The Ethereum Lottery uses BTC Relay as its RNG. While this raises the bar for miner manipulation (since Bitcoin is more expensive), it doesn’t eliminate the risk entirely.
Signidice Algorithm
Signidice is a cryptographic signature-based algorithm for RNG between two parties (player and casino):
- The player places a bet by calling the contract.
- The casino signs the bet with its private key and sends the signature to the contract.
- The contract verifies the signature with the public key.
- The signature is used to generate a random number.
Ethereum’s ecrecover()
function verifies ECDSA signatures, but ECDSA isn’t suitable for Signidice because the casino can manipulate input parameters. This vulnerability was demonstrated by Alexey Pertsev. Fortunately, the Metropolis hard fork introduced a new modular exponentiation operator, enabling RSA signature verification, which prevents such manipulation.
Commit–Reveal Approach
This approach has two stages:
- Commit: Parties submit encrypted data to the smart contract.
- Reveal: Parties reveal their seeds; the contract verifies them and uses them to generate a random number.
Neither party should be trusted. Even if players don’t know the owner’s seed, the owner could also be a player. Randao is a better implementation, collecting hashed seeds from multiple parties, each rewarded for participation. If one party refuses to reveal, it causes a DoS.
Commit–Reveal can be combined with a future block hash, using three entropy sources:
- sha3(seed1) from the owner
- sha3(seed2) from the player
- Future block hash
The random number is generated as sha3(seed1, seed2, blockhash)
. This approach solves both miner and owner motivation problems, even if the owner and miner are the same person.
Conclusion
Creating secure RNGs in Ethereum remains a challenge. As our research shows, the lack of ready-made solutions leads developers to roll their own, often making mistakes due to limited entropy sources. When building an RNG, developers must understand each party’s motivation before choosing an approach.