Ethernaut Coin Flip problem

Not sure when it happened but there a handful of new challenges up on OpenZeppelins CTF game; “The Ethernaut”. It’s a great place to start with smart contract development since all of the challenges are perfectly self contained and based on actual vulnerabilities that have lost plenty of people some ETH.

Right now, I’m gonna be breaking down the challenge titled: “Coin Flip”.

Let’s make our own luck 🍀

The CoinFlip contract
“This is a coin flipping game where you need to build up your winning streak by guessing the outcome of a coin flip. To complete this level you’ll need to use your psychic abilities to guess the correct outcome 10 times in a row.”

There’s actually no need to hack this contract! We can just flip 10 times and hope for the 0.5¹⁰ probability of guessing right 10 times in a row. It may take a while but there’s nothing like good hard repetitive work to temper the soul.

Or we can check what the result is going to be before we commit our guess.

But before that let’s just give the contract one good read through before we look at the solution.

function CoinFlip() public {
consecutiveWins = 0;
}

The constructor for this contract doesn’t do much, it just sets up the consecutiveWins counter to 0. It actually doesn’t need to do that since every variable defined but not assigned defaults to its null state (0 for uint), but it’s good for clarity anyway.

Other notable things in contract storage is the lastHash, we don’t really know what’s that for at this point but it’s worth noting it’s not part of public storage. There’s also the FACTOR a BIG ass number! It certainly looks quite ‘magical’ after some looking up it turns out this number is equal to 2²⁵⁵ (curious 🤔)

The Flippening 🐬

It looks like most of the thinking has to be done in the direction of the flip() function.

Let’s look at that function signature:

function flip(bool _guess) public returns (bool) {...

function where I give a bool and get a bool back

Moving on.

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

block.number and block.blockhash are a globally available variable and function respectively, here’s what they do:

block.number (uint): current block number

block.blockhash (function(uint) returns (bytes32)): hash of the given block — only for 256 most recent blocks

In otherwords, whenever a flip transaction is included in a block we look at the number of the previous block, we use that number to look up that previous block’s hash (which is type bytes32) and we cast that to type uint256.

How does that look like exactly? Let’s fill in the blanks just to get a notion for it:

// Current block number 30010 
// Network, Ropsten (This matters, naturally)
1. blockValue = uint256(block.blockhash((30010-1));
2. blockValue = uint256(block.blockhash((30009));
3. blockValue = uint256( 0xf5e22612a5c856807346a40b329664fb393e6ea0e585bd34cffc9f59a50353bd
);
4. blockValue = 111216218108639340416231065655650431318691078275559772203362371055421202060221

Big number is big. Important to note than casting a bytes32 to uint256 covers the same number space. Any valid uint256 can turn up out of casting bytes32.

Next up:

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

Ok. So that’s what lastHash is for. If there’s a successful transaction then lastHash is going to be set = to this blockValue which is entirely dependant on the block the transaction is being included on, and the only reasonable way for the blockValue to equal the lastHash value is for there to be multiple transactions to this function in the same block. If that’s the case only the first one will go through, that closes one avenue for us. If it weren’t for this right here we could every block with the same guess 10 times, and get the same BlockValue for every transaction.

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

3 / 4 = 0, in solidity. We round down the result of any division and throw away the remainder. Intuition should tell you that (blockValue / Factor) should be equal to 1 or to 0. And it makes sense! Bringing things down to more managable amounts, let’s treat blockValue as an uint8 and Factor as a uint7.

//Maxed out!
FACTOR = X 1 1 1 1 1 1 1
blockValue = ? ? ? ? ? ? ?

The important thing is that for blockValue to be greater than FACTOR it only needs the leftmost bit to be equal to 1, otherwise it will be less than FACTOR (or equal).

Therefor the result of this division (0 or 1) hinges on the single leftmost bit of the blockvalue, which is, ostensibly, randomly determined. Cool.

if (side == _guess){ consecutiveWins++; return true; } 
else { consecutiveWins = 0; return false; }

Finally we compare our pseudo random flip result with the user provided guess and adjust the consecutive wins accordingly. So what about, it? Anyway to PwN this contract jump out to you?

If you consider the necessity for the lastHash clause another vulnerability should become pretty obvious.

The Solution! 🏅

Every single transaction in the same block can evaluate the “coinflip” result!!

So, as it’s usually the case, the best way to hack a smart contract is with another:

Str8 outta Remix

Here we instantiate define the interface of the CoinFlip contract and instantiate it using the address of our target contract. Then, just calling the cheat function on our devious dirtyRat contract will evaluate the result of the coinflip and pass the “guess” along to the real CoinFlip contract.

This is done in a single transaction, so our CoinFlip calculations is obviously going to be the same as in the CoinFlip contract since we will necessarily be doing it in the same block.

Now all that’s left is to spam the cheat function ten times. 😎

One more thing…

There’s another equally glaring vulnerability here. Miners. Before a block becomes finalized miners build them up with different transactions and compete between themselves to commit their block to the block chain. This means they can exercise their judgment when building up blocks. A miner, given time could try many different guesses and not commit blocks where he was mistaken.

This property of Proof of Work based consensus is one of the greatest obstacles to achieving truly random outcomes on the blockchain. Think on that before building a decentralized lotto 🤑