Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Insecure Fee Handling in `buySnow()` Enables Unintended WETH Drains

Root + Impact

Description

  • Under normal behavior, the buySnow() function allows users to purchase Snow tokens by either sending ETH or paying in WETH. The intention is to require msg.value to match the fee when paying in ETH, and otherwise fallback to pulling WETH via safeTransferFrom.

  • The issue is that the contract does not verify the payment method explicitly. When msg.value doesn't match the fee, the contract blindly attempts to pull WETH, which can lead to unintended token transfers, especially if users mistakenly send ETH slightly below the expected amount. This breaks principle-of-least-surprise and could cause user losses due to poor UX or gas estimation errors.

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);
}
}

Risk

Likelihood:

  • Users frequently pay ETH with slight rounding or miscalculations, especially with frontend errors, gas overheads, or wallet UX problems.

  • WETH safeTransferFrom does not prompt the user during the contract call and could drain tokens if allowance was previously set, even unintentionally.

Impact:

  • Loss of user funds due to unintended WETH transfer, especially in dApp UIs with unclear distinction between ETH and WETH payment options.

  • Breaks expected contract behavior: user may think ETH payment failed, while WETH was deducted and tokens were minted regardless.

Proof of Concept

An attacker or regular user can unintentionally trigger the fallback to WETH payment due to a slight mismatch in msg.value. If the user has a non-zero WETH allowance approved to the Snow contract, it can be drained without explicit intent.

For example, suppose the fee per token is 1 ether (1e18 wei) and the user tries to buy 1 token:

// User wants to buy 1 token, fee = 1 WETH (1e18)
// But they accidentally send ETH = 0.9999999999 ether due to UI rounding
snow.buySnow{value: 0.9999999999 ether}(1);
// Since msg.value != s_buyFee * amount, contract pulls WETH
// If user had a leftover WETH allowance, it gets drained without confirmation

Recommended Mitigation

To avoid accidental or unintended WETH transfers, the contract should explicitly require users to choose their payment method, either ETH or WETH. This ensures the function logic does not infer intent based on msg.value and avoids unsafe assumptions.

The improved approach adds a PaymentType enum and separates logic paths:

- 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);
- }
+ enum PaymentType { ETH, WETH }
+
+ function buySnow(uint256 amount, PaymentType paymentType) external payable canFarmSnow {
+ uint256 totalFee = s_buyFee * amount;
+
+ if (paymentType == PaymentType.ETH) {
+ if (msg.value != totalFee) revert S__ZeroValue();
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), totalFee);
+ }
+
+ _mint(msg.sender, amount);
+ s_earnTimer[msg.sender] = block.timestamp;
+ emit SnowBought(msg.sender, amount);
+ }

Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 2 months ago
yeahchibyke Lead Judge about 2 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.