Snowman Merkle Airdrop

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

buySnow traps user ETH: sending non-exact msg.value charges WETH and leaves the ETH stuck with no refund

Root + Impact

## Description: buySnow lets a user pay either in native ETH (exact msg.value) or in WETH. The intent is: if the user sent the exact ETH price, mint against that ETH; otherwise pull the price in WETH. The problem is the else branch does not account for any ETH the user still sent. If msg.value is non-zero but not exactly equal to the price, execution falls into the else branch, charges the full price in WETH, and the ETH the user sent stays locked in the contract with no refund path for the user.

function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
// @> user's msg.value is ignored here; WETH is charged and the ETH is trapped
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
}

Risk

Likelihood:

* Occurs whenever a user sends any ETH that does not exactly match s_buyFee * amount while also having WETH approved — an easy mistake given gas/price variance.

Impact:

* The user pays twice: the full price in WETH plus the ETH they sent, which becomes trapped in the contract. * The trapped ETH can only be swept by the collector via collectFee(); the user has no way to recover it. Direct loss of user funds.

Proof of Concept

This test passes. The user approves WETH, calls buySnow(1) with 0.1 ETH (not the exact price), receives the token, is charged full WETH, and loses the 0.1 ETH to the contract:

function test_PoC_TrappedETH() public {
vm.startPrank(user);
weth.approve(address(snow), FEE);
snow.buySnow{value: 0.1 ether}(1);
vm.stopPrank();
assertEq(snow.balanceOf(user), 1); // got the token
assertEq(weth.balanceOf(user), 0); // full WETH charged
assertEq(address(snow).balance, 0.1 ether); // ETH trapped in contract
}

Recommended Mitigation

Reject the call if the user sent ETH that does not match the price, so they cannot accidentally pay in WETH while also losing native ETH. Alternatively, refund any non-matching msg.value at the end of the function.

function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
+ require(msg.value == 0, "send exact ETH price or no ETH");
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!