Beatland Festival

AI First Flight #4
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

# Use of `transfer` Causes ETH Withdrawals to Fail for Gas-Heavy Receivers

Use of transfer Causes ETH Withdrawals to Fail for Gas-Heavy Receivers


Description

The withdraw function transfers ETH using Solidity’s built-in transfer method:

function withdraw(address target) external onlyOwner {
@> payable(target).transfer(address(this).balance);
}

Using transfer is considered a bad practice in modern Solidity development. These methods forward a fixed gas stipend of 2300 gas to the recipient. However, due to protocol upgrades such as EIP-1884, EIP-2929, and subsequent gas repricing changes, certain operations that were previously inexpensive — most notably storage writes inside receive() or fallback()—now require more than 2300 gas.

As a result, if the target address is a smart contract whose receive() function performs any gas-expensive operation (e.g., modifying storage), the ETH transfer will revert. This makes the withdrawal mechanism unreliable and potentially unusable under valid and realistic conditions.


Risk

Likelihood: High / Impact: Medium

The likelihood is high because the withdrawal function is publicly callable by the owner and does not restrict the target address to externally owned accounts. Any attempt to withdraw ETH to a smart contract with a non-trivial receive() implementation will consistently revert. The impact is medium: while no funds are directly stolen, ETH held by the contract can become effectively locked, preventing successful withdrawals and breaking a core administrative function of the protocol.


Proof of Concept

The following proof demonstrates that withdrawing ETH fails when the recipient contract requires more than 2300 gas in its receive() function.

To reproduce the issue, we first deploy a receiver contract that performs a gas-expensive operation upon receiving ETH. As an example, we modify a storage variable, which exceeds the gas stipend provided by transfer.

contract GasHeavyReceiver {
uint256 public x;
receive() external payable {
// Storage write intentionally used to exceed the 2300 gas stipend
x = 1;
}
}

Next, we attempt to withdraw ETH from the protocol contract to this receiver:

function testWithdrawWithTransferFails() public {
// Deploy a receiver contract with a gas-heavy receive() function
GasHeavyReceiver receiver = new GasHeavyReceiver();
// Fund the FestivalPass contract with ETH
vm.deal(address(festivalPass), 1 ether);
// Attempt withdrawal as the owner
vm.prank(owner);
vm.expectRevert();
festivalPass.withdraw(address(receiver));
}

To run the test, use the following Foundry command:

forge test --match-test testWithdrawWithTransferFails

Output:

Ran 1 test for test/FestivalPass.t.sol:FestivalPassTest
[PASS] testWithdrawWithTransferFails() (gas: 91061)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.95ms (1.44ms CPU time)
Ran 1 test suite in 107.66ms (11.95ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

This confirms that under certain realistic conditions, ETH withdrawals become impossible when using transfer.


Recommended Mitigation

Replace the use of transfer with a low-level call, which forwards all remaining gas and properly handles modern gas costs. Additionally, the return value of call should be checked to ensure the transfer succeeds.

A corrected version of the withdrawal function would look as follows:

function withdraw(address target) external onlyOwner {
- payable(target).transfer(address(this).balance);
+ (bool success, ) = payable(target).call{value: address(this).balance}("");
+ require(success, "ETH transfer failed");
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!