Snowman Merkle Airdrop

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

Double-Charge Vulnerability in buySnow Payment Logic

Your report is already solid technically. What the reviewer feedback is really pointing at is structure and polish, not substance. Below is a cleaned, submission-ready rewrite that:

  • Fixes the “POW” → “PoC” issue

  • Tightens language (audit tone)

  • Improves section ordering and clarity

  • Keeps your logic and test exactly intact

  • Aligns with what Code4rena / Sherlock / Cantina reviewers expect

I’ll also call out why each change helps after.


✅ Revised, Submission-Ready Report


Double-Charge Vulnerability in buySnow Payment Logic

Summary

The buySnow function allows users to be charged twice—once in native ETH and once in WETH—when msg.value is non-zero but not exactly equal to the required purchase amount. This occurs due to ambiguous payment-path selection and missing validation that prevents mixed ETH/WETH payments.


Root Cause & Impact

The buySnow function attempts to support two payment methods:

  • Native ETH (via msg.value)

  • WETH (via safeTransferFrom)

However, the payment logic only checks for exact equality against the required fee:

if (msg.value == expectedFee) { ... } else { ... }

This creates a vulnerable state where any non-zero but incorrect msg.value causes both payment paths to be executed.


Vulnerability Details

If a caller sends any non-zero ETH amount that is not exactly equal to s_buyFee * amount, execution enters the else branch:

  • The contract keeps the ETH sent (as msg.value is always transferred)

  • The contract also pulls the full WETH amount from the caller

  • Snow tokens are minted only once

  • No refund or revert occurs

This results in a silent double charge.

Affected Code

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

Impact

Likelihood: High

  • No special permissions required

  • Triggered by a single incorrect msg.value

  • Easily reachable via UIs, routers, or calldata forwarding

Impact: High

  • Users can unknowingly overpay

  • ETH and WETH are both transferred in a single call

  • Funds are silently lost with no revert or refund

  • Breaks the invariant of a single, unambiguous payment path


Proof of Concept (PoC)

The following Foundry test demonstrates that a caller with sufficient ETH and WETH balance can be charged twice when msg.value != expectedFee.

function test_doubleChargeWhenMsgValueNotEqualExpected() external {
uint256 amount = 2;
uint256 expectedFee = snow.s_buyFee() * amount;
vm.deal(victory, 100 ether);
weth.mint(victory, 100 ether);
vm.prank(victory);
weth.approve(address(snow), type(uint256).max);
uint256 ethBefore = victory.balance;
uint256 wethBefore = weth.balanceOf(victory);
uint256 snowEthBefore = address(snow).balance;
uint256 snowWethBefore = weth.balanceOf(address(snow));
uint256 wrongMsgValue = expectedFee - 1; // non-zero, incorrect ETH amount
vm.prank(victory);
snow.buySnow{value: wrongMsgValue}(amount);
// ETH was taken
assertEq(victory.balance, ethBefore - wrongMsgValue);
// WETH was also taken
assertEq(weth.balanceOf(victory), wethBefore - expectedFee);
// Contract received both ETH and WETH
assertEq(address(snow).balance, snowEthBefore + wrongMsgValue);
assertEq(weth.balanceOf(address(snow)), snowWethBefore + expectedFee);
// Snow minted once
assertEq(snow.balanceOf(victory), amount);
}

Recommended Mitigation

Explicitly separate ETH and WETH payment paths and reject mixed payments.

Recommended Fix

function buySnow(uint256 amount) external payable canFarmSnow {
uint256 cost = s_buyFee * amount;
if (msg.value > 0) {
require(msg.value == cost, "Incorrect ETH amount");
} else {
i_weth.safeTransferFrom(msg.sender, address(this), cost);
}
_mint(msg.sender, amount);
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

This ensures:

  • ETH payments must be exact

  • WETH payments are only allowed when msg.value == 0

  • Mixed ETH/WETH payment states are impossible


Updates

Lead Judging Commences

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