Ethernaut Level 20: Denial focuses on a vulnerability related to denial-of-service (DoS) attacks. In this article, we’ll break down the Denial contract, identify its vulnerabilities, and explain how the Hack contract effectively exploits these weaknesses.
Ethernaut Challenge 20: A Case Study
In this challenge, participants are tasked with exploiting a vulnerability that can prevent the Denial contract from functioning correctly. The goal is to understand how a malicious contract can use a fallback function to cause a denial-of-service attack.
The Denial 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);
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;
}
}
Contract Analysis
- Withdrawal Mechanism: The Denial contract has a function
withdraw
that transfers 1% of the contract balance to both thepartner
and theowner
. It uses thecall
method to send Ether to thepartner
. - Vulnerability: The
withdraw
function does not handle the case where thepartner
‘s address is a contract with a fallback function that consumes all the gas, leading to a denial-of-service attack.
The Attack Contract
To exploit this vulnerability, an attacker can deploy a contract that sets itself as the partner
and uses a fallback function that runs indefinitely, consuming all the gas and causing the withdraw
function to fail.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Hack {
Denial target;
constructor(Denial _target) {
target = _target;
target.setWithdrawPartner(address(this));
}
fallback() external payable {
while(true){}
}
}
Attack Strategy
- Set the Partner: The attacker deploys the Hack contract and sets it as the
partner
in the Denial contract using thesetWithdrawPartner
function. - Consume Gas in Fallback: The Hack contract’s fallback function is designed to run an infinite loop, consuming all the gas and preventing the
withdraw
function in the Denial contract from completing.
Step-by-Step Execution
- Deploy the Hack Contract:
- The attacker deploys the Hack contract with the Denial contract’s address as a parameter.
- This sets the Hack contract as the
partner
in the Denial contract.
- Infinite Loop in Fallback:
- When the Denial contract calls
partner.call{value: amountToSend}("")
in thewithdraw
function, it triggers the fallback function of the Hack contract. - The fallback function runs an infinite loop, consuming all the gas and causing the
withdraw
function to fail.
- When the Denial contract calls
Why This Works
- The
call
method in Solidity does not propagate errors, so if thepartner
‘s contract consumes all the gas, the subsequent operations in thewithdraw
function will not execute. - By consuming all the gas, the Hack contract effectively prevents the Denial contract from making any further withdrawals, causing a denial-of-service condition.
Conclusion: Ethernaut Level 20
The Denial challenge walkthrough demonstrates the importance of handling external calls carefully in smart contracts. By exploiting the fallback function to consume all the gas, an attacker can prevent the Denial contract from functioning correctly. This highlights the need for developers to implement proper gas management and error handling in their contracts.
Check out the rest of the Ethernaut Challenges here.
FAQs
What should developers learn from this challenge?
- Developers should learn the importance of proper gas management and error handling when making external calls in smart contracts. They should also understand the potential risks of denial-of-service attacks and implement safeguards to prevent them.
What is the difference between call, send, and transfer in Solidity?
- call: Low-level function that forwards all available gas to the recipient. It is very flexible but requires careful handling of return values and gas management. It also allows contract-to-contract communication.
- send: Forwards a fixed amount of gas (2300 units) to the recipient. It returns a boolean indicating success or failure but does not revert the transaction on failure.
- transfer: Similar to send, it forwards a fixed amount of gas (2300 units) and reverts the transaction on failure.
How can this vulnerability be prevented?
- Use the call function with an error check:
solidity (bool sent, bytes memory data) = partner.call{value: amountToSend}(""); require(sent, "Transaction failed");
This ensures that if the call fails, the transaction is reverted, preventing the fallback function in a malicious contract from consuming all the gas and causing a DoS condition.