Beatland Festival

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

FestivalPass::withdraw uses transfer() and lacks safety checks, risking stuck ETH and failed withdrawals

Root + Impact

Description

withdraw(address target) pays out the entire contract balance with Solidity’s transfer(), which:

  • Forwards only 2 300 gas → reverts for most multisig / module wallets.

  • Has become fragile after EIP-1884; even EOAs wrapped in smart-accounts can fail.

  • Lacks target != address(0) guard — ETH can be burned.

Result: Festival revenue can be stuck or burned, blocking withdrawals until a contract upgrade or redeploy.

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

Risk

Likelihood:

Organizers typically route funds to Gnosis Safes or payment-splitter contracts that need >2 300 gas → call reverts.

Impact:

ETH locked in FestivalPass. Operational disruption during event. Potential permanent loss if zero address supplied.

Proof of Concept

The PoC spins up an ExpensiveReceiver whose receive() deliberately uses > 2 300 gas. When withdraw() attempts to pay this address the call reverts, proving that the current transfer()based implementation can lock funds.

contract ExpensiveReceiver { receive() external payable { for (uint i; i<100; ++i) {} } }
function test_stuckFunds() public {
ExpensiveReceiver recv = new ExpensiveReceiver();
vm.deal(address(festivalPass), 1 ether);
vm.prank(festivalPass.owner());
vm.expectRevert(); // transfer fails
festivalPass.withdraw(address(recv));
}

Recommended Mitigation

Switching to a low-level call forwards all remaining gas, checks success, and blocks the zero-address. This makes withdrawals compatible with multisigs and smart-account wallets and removes the 2 300 gas fragility.

- payable(target).transfer(address(this).balance);
+ require(target != address(0), "zero target");
+ uint256 amount = address(this).balance;
+ (bool ok, ) = payable(target).call{value: amount}("");
+ require(ok, "ETH transfer failed");
+ emit Withdraw(target, amount);
Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Zero address check

Owner/admin is trusted / Zero address check - Informational

Support

FAQs

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