Beatland Festival

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

Unsafe ETH transfer in `withdraw()` can permanently lock funds

M-1: Unsafe ETH transfer in withdraw() can permanently lock funds

Description

The withdraw() function allows the owner to extract collected ETH from pass sales to a specified target address. This function is critical for festival revenue collection.

However, the function uses Solidity's transfer() method, which forwards only 2300 gas to the recipient. If the target address is a contract with a fallback function that requires more than 2300 gas, the transfer will fail and revert. This can permanently lock all ETH in the contract if the owner address is changed to a contract or if the intended withdrawal target requires more gas.

function withdraw(address target) external onlyOwner {
@> payable(target).transfer(address(this).balance); // transfer() has fixed 2300 gas limit
}

Risk

Likelihood: Medium

  • Occurs whenever target is a contract with non-trivial fallback/receive function

  • Common with multisig wallets, smart contract wallets, or contracts that log events

  • Owner might not realize target contract is incompatible until funds are already locked

  • Future contract upgrades or chain gas cost changes could break previously working targets

Impact: High

  • All collected ETH from pass sales becomes permanently locked in contract

  • No alternative withdrawal mechanism exists in the contract

  • Financial loss proportional to total pass sales revenue

  • Contract would require emergency upgrade or migration to recover funds

Proof of Concept

// Scenario: Owner uses Gnosis Safe multisig as withdrawal target
contract GnosisSafeSimulator {
// Gnosis Safe fallback might emit events or perform checks
receive() external payable {
// Simulating operations that cost > 2300 gas
for (uint i = 0; i < 10; i++) {
// Storage writes cost more than 2300 gas
someMapping[i] = msg.value;
}
}
mapping(uint => uint) public someMapping;
}
// Test execution:
GnosisSafeSimulator safe = new GnosisSafeSimulator();
// FestivalPass has collected 100 ETH from pass sales
// balance = 100 ETH
festivalPass.withdraw(address(safe));
// REVERTS: out of gas
// The 100 ETH is now permanently stuck in FestivalPass contract
// No other function can extract the funds

Recommended Mitigation

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

Alternative with pull pattern for additional safety:

+ mapping(address => uint256) public pendingWithdrawals;
+ function initiateWithdrawal(uint256 amount) external onlyOwner {
+ require(amount <= address(this).balance, "Insufficient balance");
+ pendingWithdrawals[msg.sender] += amount;
+ }
+ function claimWithdrawal() external {
+ uint256 amount = pendingWithdrawals[msg.sender];
+ require(amount > 0, "No pending withdrawal");
+ pendingWithdrawals[msg.sender] = 0;
+ (bool success, ) = payable(msg.sender).call{value: amount}("");
+ require(success, "Transfer failed");
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 14 days 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!