Beatland Festival

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

Use of `transfer()` in `withdraw` Function Can Lead to Locked Funds

Description

  • The withdraw function is used by the contract owner to retrieve the ETH collected from pass sales.

  • It uses the payable.transfer() function, which has a hardcoded gas stipend of 2300. If the recipient (target) is a smart contract that requires more than 2300 gas for its receive() or fallback() function (e.g., a multi-sig wallet), the transfer will fail.

// src/FestivalPass.sol
// Organizer withdraws ETH
function withdraw(address target) external onlyOwner {
@> payable(target).transfer(address(this).balance);
}

Risk

Likelihood:

  • The owner of the contract is a multi-sig wallet (like Gnosis Safe) or another smart contract that is commonly used for managing funds. These contracts often have fallback functions that consume more than 2300 gas.

Impact:

  • All ETH collected from pass sales will be permanently locked within the FestivalPass contract. There is no other mechanism to withdraw the funds, so they will be lost forever.

Proof of Concept

// A simple receiver contract that consumes more than 2300 gas.
contract GasGuzzler {
uint256 public value;
receive() external payable {
// A simple storage write will exceed the 2300 gas limit.
value = msg.value;
}
}
function test_poc_withdraw_fails_for_contract() public {
// 1. A user buys a pass,
vm.prank(user1);
festivalPass.buyPass{value: GENERAL_PRICE}(1);
assertEq(address(festivalPass).balance, GENERAL_PRICE);
// 2. The owner tries to withdraw the funds to a GasGuzzler contract.
GasGuzzler guzzler = new GasGuzzler();
vm.prank(owner);
// 3. The withdraw call reverts because the transfer fails due to the gas limit.
vm.expectRevert();
festivalPass.withdraw(address(guzzler));
// 4. The balance remains locked in the FestivalPass contract.
assertEq(address(festivalPass).balance, GENERAL_PRICE);
}

Recommended Mitigation

// In FestivalPass.sol
// Organizer withdraws ETH
- function withdraw(address target) external onlyOwner {
- payable(target).transfer(address(this).balance);
- }
+ function withdraw(address target) external onlyOwner {
+ (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.