Snowman Merkle Airdrop

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

buySnow does not revert on mismatched msg.value, causing user ETH to be permanently stuck

Root + Impact

Description

buySnow uses an exact equality check (msg.value == s_buyFee * amount) to determine whether the caller is paying in raw ETH. If the check fails, the function silently falls through to the WETH path without reverting or refunding the sent ETH.

The specific issue is that any non-zero msg.value that doesn't exactly match s_buyFee * amount is accepted, the WETH pull executes on top of it, and the stray ETH has no refund mechanism — it is permanently locked in the contract.

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);
}
// no revert or refund for mismatched msg.value
}

Risk

Likelihood:

  • Any buyer who miscalculates the fee (e.g., sends 3 ETH when 5 is required) will trigger the else branch while their ETH sits in the contract.

  • Frontends or scripts that round or truncate values before computing msg.value will produce mismatches.

Impact:

  • User's ETH is permanently trapped in the contract.

  • User pays the full WETH fee on top of the already-sent ETH, effectively overpaying by the amount of msg.value.

Proof of Concept

The equality check msg.value == (s_buyFee * amount) evaluates to 3 ether == 5 etherfalse. Execution falls into the else branch, where safeTransferFrom pulls 5 WETH from the buyer's wallet. The transaction succeeds, 5 Snow tokens are minted, but the 3 ETH sent with the call has no refund path — it is permanently locked in the contract. The buyer has now paid 8 ETH equivalent (3 ETH + 5 WETH) for tokens worth 5 ETH.

// Deployer sets _buyFee = 1, so s_buyFee = 1e18 (1 ETH per token)
// Buyer wants 5 tokens, total cost = 5 ETH
// Buyer sends 3 ETH (miscalculation)
contract.buySnow{ value: 3 ether }(5);
// msg.value (3 ETH) != s_buyFee * amount (5 ETH) → else branch
// Contract pulls 5 WETH from buyer via safeTransferFrom
// 3 ETH is now permanently stuck in the contract
// Buyer paid 3 ETH + 5 WETH for 5 tokens

Recommended Mitigation

This guard ensures that if the caller sends any ETH that doesn't exactly match the expected fee, the transaction reverts immediately and the ETH is returned to the sender. The two legitimate paths remain intact:

  • msg.value == 0 → WETH path (no ETH was sent, so nothing to validate)

  • msg.value == s_buyFee * amount → raw ETH path (exact match, proceeds normally)

Any other value is rejected before the WETH pull can execute, eliminating the stuck-ETH scenario entirely.

function buySnow(uint256 amount) external payable canFarmSnow {
+ if (msg.value > 0 && msg.value != (s_buyFee * amount)) {
+ revert("Incorrect ETH amount");
+ }
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);
}
Updates

Lead Judging Commences

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