Beatland Festival

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

`withdraw()` uses `transfer()` which can fail for smart contract recipients

Description

Root + Impact

  • The withdraw() function uses Solidity's .transfer() to send ETH to the target address. This forwards a fixed 2300 gas stipend to the recipient.

  • If target is a smart contract (e.g., a multisig wallet like Gnosis Safe, or a DAO treasury), the 2300 gas may not be enough for the recipient's receive() or fallback() function, causing the withdrawal to permanently revert.

function withdraw(address target) external onlyOwner {
payable(target).transfer(address(this).balance); // @> Only 2300 gas forwarded — fails for many smart contract recipients
}

Risk

Likelihood:

  • Many protocol owners use multisig wallets (Gnosis Safe) as the owner address, and would logically set the withdrawal target to another smart contract (treasury, splitter)

  • Gnosis Safe's receive() function requires more than 2300 gas

Impact:

  • All ETH from pass sales becomes permanently locked in the FestivalPass contract if the target is a contract that needs more than 2300 gas

  • The owner cannot change the withdrawal mechanism — there is no alternative withdrawal function


Proof of Concept

contract GasHungryReceiver {
uint256 public received;
receive() external payable {
received += msg.value; // SSTORE costs 5000+ gas, exceeds 2300 stipend
}
}
function test_WithdrawFailsForContractTarget() public {
// User buys pass — ETH goes to FestivalPass
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
// Deploy a contract target (simulating multisig treasury)
GasHungryReceiver receiver = new GasHungryReceiver();
// Withdraw reverts — 2300 gas insufficient
vm.expectRevert();
festivalPass.withdraw(address(receiver));
console.log("ETH permanently stuck:", address(festivalPass).balance);
}

Recommended Mitigation

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 2 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!