Snowman Merkle Airdrop

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

Incorrect Payment Logic in buySnow() Can Lock User Funds

Root + Impact

Description

The buySnow() function has flawed payment logic that can result in users losing their ETH. When a user sends ETH but the amount doesn't exactly match the required fee, the function falls through to attempt a WETH transfer instead of reverting. This leaves the user's ETH stuck in the contract while also charging them WETH, resulting in double payment.

The function checks if msg.value equals the exact fee amount, and if not, it proceeds to transfer WETH from the user without refunding or reverting the ETH transaction.

// src/Snow.sol:79-90
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
// @> Falls through to WETH transfer even if ETH was sent (wrong amount)
// @> User's ETH gets stuck in contract!
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood:

  • Users can easily send incorrect ETH amounts due to UI errors or miscalculation

  • The s_buyFee is multiplied by PRECISION (10^18) which may not be obvious to users

  • No warning or revert when incorrect ETH amount is sent

Impact:

  • User's ETH becomes permanently locked in the Snow contract

  • User is charged both ETH (stuck) and WETH for the same purchase

  • No mechanism exists to recover the stuck ETH

  • Poor user experience and potential fund loss

Proof of Concept

function testFundsGetLocked() public {
// Setup: s_buyFee = 1 ether, user wants to buy 10 Snow tokens
uint256 correctPayment = 10 ether;
// User accidentally sends 9 ether instead of 10
vm.deal(alice, 9 ether);
vm.startPrank(alice);
// Alice also has WETH and has approved the contract
weth.approve(address(snow), 10 ether);
// Alice calls buySnow with incorrect ETH amount
snow.buySnow{value: 9 ether}(10);
// Result:
// - 9 ETH stuck in Snow contract ❌
// - 10 WETH transferred from Alice ❌
// - Alice paid 9 ETH + 10 WETH for 10 Snow tokens
// - Should only pay 10 ETH OR 10 WETH
assertEq(address(snow).balance, 9 ether); // ETH stuck
assertEq(weth.balanceOf(address(snow)), 10 ether); // WETH charged
// Alice lost 9 ETH permanently!
}

Recommended Mitigation

+ error S__IncorrectPayment();
function buySnow(uint256 amount) external payable canFarmSnow {
uint256 totalCost = s_buyFee * amount;
if (msg.value > 0) {
+ if (msg.value != totalCost) {
+ revert S__IncorrectPayment();
+ }
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), totalCost);
_mint(msg.sender, amount);
}
- s_earnTimer = block.timestamp;
+ s_earnTimer[msg.sender] = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

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