Ethernaut Level 20: Denial
Solidity Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Denial {
using SafeMath for uint256;
address public partner; // withdrawal partner - pay the gas, split the withdraw
address payable 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.div(100);
// perform a call without checking return
// The recipient can revert, the owner will still get their share
partner.call{value:amountToSend}("");
owner.transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint) {
return address(this).balance;
}
}
Requirements
- Deny owner from withdrawing funds when calling
withdraw
.
Concepts
- Difference between transfer, call, and send.
- Difference between assert, require and revert.
Hack
Examining the function withdraw
specifically at lines
partner.call{value:amountToSend}("");
owner.transfer(amountToSend);
we notice that 2 different methods of transfer is used, call
and transfer
. An important difference between them is how they handle gas. call
forwards all the gas or set gas meanwhile transfer
forwards 2300 gas. This is important because if we are able to drain out all the gas in the call
function, we can prevent transfer
from executing.
Solution
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract DenialAttack {
receive() external payable {
assert(false);
}
}
We create a malicious contract that will become the partner
of the Denial
smart contract. This will have a fallback
function that will use up all the gas sent to it. This is possible using the assert
error handling.
It compiles to
0xfe
, which is an invalid opcode, using up all remaining gas, and reverting all changes.
After deploying the DenialAttack
, we execute the following command.
await contract.setWithdrawPartner(DenialAttackAddress)
When the owner attempts to transfer funds to their account, it will fail because all the gas is used up by then.
Done!