Beatland Festival

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

[ H-2] - Use of transfer for Withdrawals + Locked Funds Impact

Use of transfer() for Withdrawals + Locked Funds Impact

Description

  • Normally, a withdrawal function should safely transfer ETH to a specified address, regardless of whether the recipient is an externally owned account (EOA) or a smart contract.

  • In this implementation, the withdraw function uses payable(target).transfer(address(this).balance), which forwards only 2,300 gas to the recipient. If the recipient is a contract with a fallback or receive function that requires more gas (such as a multi-sig wallet or upgradeable contract), the transfer will fail and the funds will remain locked in the contract.

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

Risk

Likelihood:

  • This will occur when the withdrawal target is a contract with a fallback/receive function that needs more than 2,300 gas.

  • Many modern multi-sig wallets and upgradeable contracts require more gas, so this is a realistic scenario in production.

Impact:

  • The contract's ETH balance can become permanently locked, making it impossible for the owner to withdraw funds.

  • Users and protocol operators may lose access to significant amounts of ETH, leading to financial loss and loss of trust.

Proof of Concept

Past the following test to FestivalPass.t.sol::FestivalPassTestto demonstrate the issue. It deploys a GasHeavyReceiver contract whose receive function requires more than 2,300 gas. When withdraw is called with this contract as the target, the transaction reverts and the funds remain locked in the contract.

function test_Vulnerability_UnsafeWithdrawalMechanism() public {
vm.deal(address(festivalPass), 1 ether);
assertEq(address(festivalPass).balance, 1 ether);
GasHeavyReceiver gasHeavyReceiver = new GasHeavyReceiver();
vm.expectRevert();
festivalPass.withdraw(address(gasHeavyReceiver));
assertEq(address(festivalPass).balance, 1 ether);
assertEq(address(gasHeavyReceiver).balance, 0);
}

Explanation:
This test shows that when the withdrawal target is a contract with a gas-heavy receive function, the withdrawal fails and the contract's balance remains unchanged, proving the risk of permanent fund lockup.

Recommended Mitigation

Use the recommended check-effects-interactions pattern with .call() to forward all available gas and handle errors safely.

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

Lead Judging Commences

inallhonesty Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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