Snowman Merkle Airdrop

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

buySnow() keeps any non-exact ETH payment and still charges the full WETH price on top, so inexact payers pay twice

Root + Impact

buySnow branches on an exact equality: only if msg.value == s_buyFee * amount does it treat the call as an ETH purchase. Any other value, including a nonzero ETH amount that is slightly more or less than the fee, falls into the WETH branch, which pulls the full fee in WETH via safeTransferFrom while the ETH that accompanied the call simply stays in the contract. The buyer thus pays the full price in WETH plus whatever ETH they sent, and the stray ETH is later swept to the fee collector by collectFee, who receives it as if it were fee revenue. There is no refund path and no error: a user who misjudges the exact wei amount (easy, since the fee scales with PRECISION and the amount) silently loses the entire ETH portion of their payment.

Description

function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) { // exact match only
_mint(msg.sender, amount);
} else {
// @> i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount); // msg.value is kept by the contract either way
}
...
}

Risk

Likelihood: Medium - requires a buyer to attach a nonzero but inexact ETH amount, a realistic mistake for a payable function whose price is computed from stored parameters; nothing warns or reverts when it happens.
Impact: Medium - direct loss of the full ETH amount the user sent (up to nearly the whole purchase price), on top of the full WETH charge; the funds end up with the collector rather than being refunded or credited.

Proof of Concept

Verified with Foundry (test passes). Dave buys 1 Snow (fee = 1 ether worth) attaching 0.5 ether, half the fee, intending a partial ETH payment. The contract keeps his 0.5 ether AND pulls the full 1e18 WETH fee from him, minting 1 Snow.

snow.buySnow{value: 0.5 ether}(1);
assertEq(weth.balanceOf(address(snow)), 1e18); // full WETH fee also taken
assertEq(address(snow).balance, 0.5 ether); // ETH kept, never refunded

Recommended Mitigation

Make the payment method explicit and safe: if msg.value > 0, require it to cover the fee and refund any excess (or revert unless it matches exactly), and only fall back to WETH when msg.value == 0.

Updates

Lead Judging Commences

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