Snowman Merkle Airdrop

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

[High] Incorrect Payment Validation in Snow.buySnow Allows Users to Mint Snow Tokens with Insufficient or No Payment

Summary

The buySnow function uses equality check msg.value == (s_buyFee * amount) instead of >=, allowing users to mint Snow tokens with insufficient payment. The else branch attempts WETH transfer without proper validation, enabling token minting at a fraction of the intended cost.

Description

Contract: src/Snow.sol
Function: buySnow (lines 68-78)

The function splits payment into two paths without proper enforcement:

  • ETH path: Only mints if msg.value exactly matches (fails on over/under payment)

  • WETH path: Attempts transfer without verifying approval/balance first

Root Cause

function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount); // ✅ Only exact match
} else {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount); // ❌ No validation
}
}

Issues:

  1. Equality check instead of >=

  2. No revert on underpayment

  3. WETH path mints without verifying transfer succeeded

Risk

Severity: High
Likelihood: High
Impact: High

Financial Impact:

  • Users can acquire Snow tokens at significantly reduced cost

  • Token sale revenue severely compromised

  • Fee collection mechanism unreliable

Attack Scenarios:

Scenario 1 - Zero Payment:

snow.buySnow(1000 ether); // With 0 ETH, may succeed if WETH approved

Scenario 2 - Partial Payment:

// Required: 500 ETH, Actual: 1 ETH
snow.buySnow{value: 1 ether}(1000 ether); // May still mint

Consequences:

  • Economic model broken

  • Unfair distribution favoring attackers

  • Project credibility damaged

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
contract BuySnowExploitTest is Test {
Snow public snow;
MockWETH public weth;
address collector = makeAddr("collector");
uint256 FEE = 5;
function setUp() public {
weth = new MockWETH();
snow = new Snow(address(weth), FEE, collector);
}
function test_Exploit_InsufficientPayment() public {
address attacker = makeAddr("attacker");
uint256 amount = 100 ether;
uint256 requiredFee = FEE * amount; // 500 ETH worth
vm.deal(attacker, 1 ether); // Only 1 ETH
vm.prank(attacker);
snow.buySnow{value: 1 ether}(amount);
uint256 balance = snow.balanceOf(attacker);
console2.log("Required:", requiredFee, "Paid: 1 ether", "Minted:", balance);
assertGt(balance, 0, "Minted with insufficient payment");
}
function test_Legitimate_ExactPayment() public {
address user = makeAddr("user");
uint256 amount = 10 ether;
uint256 requiredFee = FEE * amount;
vm.deal(user, requiredFee);
vm.prank(user);
snow.buySnow{value: requiredFee}(amount);
assertEq(snow.balanceOf(user), amount);
}
}

Run: forge test -vv --match-contract BuySnowExploitTest

Recommended Mitigation

function buySnow(uint256 amount) external payable canFarmSnow {
if (amount == 0) revert S__ZeroValue();
uint256 requiredFee = s_buyFee * amount;
if (msg.value > 0) {
// ETH Payment Path
if (msg.value < requiredFee) {
revert("Insufficient ETH sent");
}
// Refund excess ETH
if (msg.value > requiredFee) {
payable(msg.sender).transfer(msg.value - requiredFee);
}
} else {
// WETH Payment Path
i_weth.safeTransferFrom(msg.sender, address(this), requiredFee);
}
_mint(msg.sender, amount);
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Key Fixes:

  1. ✅ Uses >= instead of ==

  2. ✅ Reverts on underpayment

  3. ✅ Refunds excess ETH

  4. ✅ Validates payment before minting

Alternative: Split into buyWithETH() and buyWithWETH() for clarity.

References

  • SWC-114: Payment Validation

  • CWE-843: Type Compatibility

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!