Ethernaut#9 - King
Ethernaut #9 - King
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 | // SPDX-License-Identifier: MIT |
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 | address king; |
king stores the address of the current king. prize is the current amount needed to become king. owner is the contract deployer.
Constructor
1 | constructor() payable { |
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 | receive() external payable { |
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 | function _king() public view returns (address) { |
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?
transferforwards only 2300 gas and reverts the entire transaction if the transfer fails. If we become king using a contract that has noreceiveorfallbackfunction (or one that reverts), then anyone trying to become the new king will fail because thetransferto our contract will revert.
1 | // SPDX-License-Identifier: MIT |
Steps to pass:
- Check the current
prizeby callingprize()on the King contract through Remix or the console. - Copy the
HackKingcontract 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
HackKingcontract becomes the new king. - Now when anyone else (including the owner when Ethernaut tries to reclaim kingship) tries to send more ether, the
transferto ourHackKingcontract 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
transferorsendwill 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.