Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Permanent Lock of ETH in Snow.sol

Root + Impact

Description

  • In a standard token sale model, the protocol owner or a treasury address should be able to withdraw accumulated ETH generated from sales to manage project liquidity and operational costs

  • The buySnow function is marked as payable and correctly accepts ETH from users in exchange for tokens. However, the Snow.sol contract lacks any withdrawal mechanism (such as a withdraw() function) or low-level implementation to transfer funds out of the contract

// src/Snow.sol
@> function buySnow(uint256 amount) external payable {
if (msg.value != s_buyFee * amount) {
revert Snow__WrongAmount();
}
_mint(msg.sender, amount);
}
// @> Root Cause: No logic exists in the contract to move the collected ETH balance to the owner or treasury.

Risk

Likelihood:

  • Users interact with the payable function during the primary token acquisition phase.

  • The total lack of administrative rescue logic makes the fund lock inevitable upon the first successful purchase

Impact:

  • 100% of the ETH revenue is permanently trapped in the contract

  • The protocol suffers a total loss of all sale-generated capital, rendering the "Buy" feature financially destructive for the project

Proof of Concept

function testETHBlackHole() public {
address protocolOwner = snow.owner();
uint256 ownerInitialBalance = protocolOwner.balance;
uint256 priceSnow = 5 ether;
vm.deal(alice, 10 ether);
vm.prank(alice);
snow.buySnow{value: priceSnow}(1);
// Impact: Alice loses 5 ETH, Contract gains 5 ETH, but Owner remains at 0
(bool success, ) = address(snow).call(abi.encodeWithSignature("withdraw()"));
assertEq(success, false, "Withdraw should fail as function does not exist");
assertEq(protocolOwner.balance, ownerInitialBalance, "Owner balance remains unchanged");
assertEq(address(snow).balance, 5 ether, "ETH remains locked in the contract");
}

Recommended Mitigation

+ error Snow__WithdrawFailed();
+ function withdraw() external onlyOwner {
+ (bool success, ) = payable(owner()).call{value: address(this).balance}("");
+ if (!success) {
+ revert Snow__WithdrawFailed();
+ }
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 19 hours 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!