Snowman Merkle Airdrop

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

buySnow() Traps User ETH and Double Charges via WETH on Incorrect Payment

Root + Impact

Description

  • buySnow() is designed to let users purchase SNOW tokens by paying either exact ETH or WETH equal to s_buyFee * amount.

  • When a user sends ETH that does not exactly match the required amount, the contract silently retains the sent ETH without refunding it, then also pulls the full cost in WETH from the user — resulting in the user being charged twice and permanently losing their ETH.

function buySnow(uint256 amount) external payable canFarmSnow {
// @> Only passes if msg.value is EXACT — any deviation falls to else
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
// @> ETH already received by contract — NOT refunded here
// @> Contract ALSO pulls full WETH cost on top of trapped ETH
i_weth.safeTransferFrom(msg.sender, address(this), s_buyFee * amount);
_mint(msg.sender, amount);
}
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood: High

  • Any user sending a slightly incorrect ETH amount due to fee estimation errors, UI rounding, or decimal mistakes triggers the double charge automatically

  • The condition msg.value == exact amount has zero tolerance — partial ETH sends are common in real usage and all of them hit this bug

Impact: High

  • Users permanently lose their sent ETH — it is never refunded and is later swept by collectFee() to the collector

  • Users are double-charged, losing sent ETH AND having full WETH amount pulled, paying up to 2x the intended price for their tokens

Proof of Concept

function test_UserDoubleCharged() public {
// buyFee = 1 ether, user wants 1 Snow token
// User mistakenly sends 0.5 ETH instead of 1 ETH
uint256 wrongEthAmount = 0.5 ether;
uint256 wethRequired = 1 ether;
deal(address(weth), user, wethRequired);
vm.startPrank(user);
weth.approve(address(snow), wethRequired);
// User sends wrong ETH amount
snow.buySnow{value: wrongEthAmount}(1);
// Contract kept 0.5 ETH AND took 1 ETH WETH
// User paid 1.5 ETH total for 1 Snow token
assertEq(address(snow).balance, 0.5 ether); // ETH trapped
assertEq(weth.balanceOf(address(snow)), 1 ether); // WETH also taken
assertEq(snow.balanceOf(user), 1e18); // only 1 token received
vm.stopPrank();
}

Recommended Mitigation

function buySnow(uint256 amount) external payable canFarmSnow {
+ uint256 totalCost = s_buyFee * amount;
+ if (msg.value > 0 && msg.value != totalCost) {
+ revert Snow__WrongETHAmount();
+ }
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
+ require(msg.value == 0, "Use ETH or WETH, not both");
i_weth.safeTransferFrom(msg.sender, address(this), s_buyFee * amount);
_mint(msg.sender, amount);
}
emit SnowBought(msg.sender, amount);
}
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!