Snowman Merkle Airdrop

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

Snow::buySnow() does not reject zero amount and fails to refund native ETH when charging WETH

Root + Impact

Description

In src/Snow.sol, buySnow(uint256 amount) allows amount == 0 and, when msg.value != s_buyFee * amount, charges the user full WETH via safeTransferFrom without refunding or rejecting non-zero msg.value.

In src/Snow.sol (lines 79–90):

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

If a user sends msg.value > 0 that is not equal to s_buyFee * amount, the else branch pulls the full fee in WETH while keeping the sent native ETH trapped in the contract (later swept by the fee collector).

Risk

Likelihood:

  • Any user who overpays or underpays native ETH while holding WETH allowance loses both their ETH and WETH, and buySnow(0) succeeds with zero payment.

Impact:

  • Direct loss of user funds (msg.value double-charged alongside WETH) and zero-cost state manipulation via buySnow(0).

Proof of Concept

Add the following test to test/TestSnow.t.sol demonstrating how a user is double-charged in both ETH and WETH:

function testBuySnowDoubleChargesEthAndWeth() public {
deal(jerry, 0.5 ether);
vm.startPrank(jerry);
weth.approve(address(snow), FEE);
// Jerry sends 0.5 ETH (less than FEE) while having WETH approved
snow.buySnow{value: 0.5 ether}(1);
vm.stopPrank();
​
// Both WETH and 0.5 ETH were taken from Jerry!
assertEq(weth.balanceOf(address(snow)), FEE);
assertEq(address(snow).balance, 0.5 ether);
}

Recommended Mitigation

Validate that amount > 0 at the beginning of buySnow() to prevent zero-value calls. When msg.value > 0, require that msg.value == s_buyFee * amount (or revert in the else branch if msg.value != 0) so that users paying with WETH cannot accidentally have their native ETH locked in the contract.

function buySnow(uint256 amount) external payable canFarmSnow {
+ if (amount == 0) revert S__ZeroValue();
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
+ if (msg.value != 0) revert S__ZeroValue();
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
Updates

Lead Judging Commences

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