Snowman Merkle Airdrop

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

ETH/WETH Payment Logic Allows Partial ETH to Be Stuck

Root + Impact

The buySnow function's conditional logic causes users who send partial ETH to have their ETH stuck in the contract while also being charged the full WETH amount.

Description

The buySnow function should clearly separate ETH and WETH payment paths. Users should pay either the exact ETH amount or the WETH amount, but not a combination.

The function checks if msg.value equals the required fee; if not, it attempts a WETH transfer. A user sending partial ETH will have their ETH accepted AND be charged WETH, with the ETH remaining stuck in the contract.

// Snow.sol
function buySnow(uint256 amount) external payable canFarmSnow {
// @> If msg.value is partial (not 0 and not exact), falls through to WETH path
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
// @> User's partial ETH is now stuck, AND they pay WETH
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
// ...
}

Risk

Likelihood:MEDIUM

  • Reason 1 :Users sending incorrect ETH amounts accidentally will trigger this

  • Reason 2:UI bugs or user errors in calculating exact amounts cause stuck funds

Impact:MEDIUM

  • Impact 1:Users pay more than intended (partial ETH + full WETH)

  • Impact 2: ETH becomes stuck until collector withdraws

Proof of Concept

This test shows a user accidentally sending half the required ETH. The transaction doesn't revert but instead falls through to the WETH path. The user ends up paying both the partial ETH (which stays in the contract) and the full WETH amount, effectively double-paying.

function testPartialETHGetsStuck() public {
address user = makeAddr("user");
uint256 partialETH = buyFee / 2;
deal(user, partialETH);
deal(address(weth), user, buyFee);
vm.startPrank(user);
weth.approve(address(snow), buyFee);
snow.buySnow{value: partialETH}(1);
vm.stopPrank();
// User paid both partialETH and full WETH
assertEq(address(snow).balance, partialETH);
assertEq(weth.balanceOf(address(snow)), buyFee);
}

Recommended Mitigation

Separate the function into two distinct functions for ETH and WETH payments. This eliminates ambiguity and ensures users explicitly choose their payment method, preventing accidental double-payments or stuck funds.

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

Lead Judging Commences

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