Snowman Merkle Airdrop

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

`Snow::buySnow` - ETH sent with mismatched fee is not refunded, user loses ETH and also pays WETH

Root + Impact

Description

  • In buySnow, if msg.value is greater than 0 but does not exactly equal s_buyFee * amount, the else branch executes which charges the user WETH via safeTransferFrom.

  • The ETH sent with the transaction is not refunded. The user ends up paying both ETH (stuck in contract) and WETH for the same purchase.

  • While the ETH can eventually be recovered by the collector via collectFee, the user who sent it loses their ETH.

// src/Snow.sol
function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) { // @> exact match required
_mint(msg.sender, amount);
} else {
// @> if msg.value > 0 but wrong amount, ETH is NOT refunded
// @> user ALSO pays WETH here
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood:

  • A user who miscalculates the exact ETH fee, or a frontend that sends slightly wrong amounts, triggers this.

  • Less likely for sophisticated users, but very possible for newcomers.

Impact:

  • User loses the ETH sent (it goes to the collector, not back to user).

  • User also pays the full WETH fee - double payment for one purchase.


Proof of Concept

The buySnow function uses an if/else structure where the if branch checks for an exact ETH amount match. If the ETH sent does not exactly equal s_buyFee * amount, the else branch executes, which pulls WETH from the user via safeTransferFrom. The problem is that the ETH sent in msg.value is never refunded in the else branch. The user ends up paying twice — their ETH stays in the contract and they also pay the full WETH amount.

Step-by-step scenario:

  1. The buy fee is set to 5 (i.e. s_buyFee = 5 * 10^18). To buy 1 Snow token with ETH, a user must send exactly 5 * 10^18 wei.

  2. A user mistakenly sends 4.9 * 10^18 wei (slightly less than required) when calling buySnow(1).

  3. The if condition msg.value == (s_buyFee * amount) evaluates to false because 4.9e18 != 5e18.

  4. The else branch executes: i_weth.safeTransferFrom(user, contract, 5e18) pulls 5 WETH from the user.

  5. The user receives 1 Snow token, but they paid 4.9 ETH + 5 WETH for it.

  6. The 4.9 ETH remains in the Snow contract and will be collected by the fee collector via collectFee(). The user has no way to recover it.

function testETHStuckOnMismatch() public {
address user = makeAddr("user");
uint256 fee = snow.s_buyFee();
// Give user ETH and WETH
vm.deal(user, 10 ether);
deal(address(weth), user, 10 ether);
// User approves WETH spending
vm.prank(user);
weth.approve(address(snow), type(uint256).max);
uint256 userETHBefore = user.balance;
uint256 userWETHBefore = weth.balanceOf(user);
// Step 1: User sends slightly wrong ETH amount
uint256 wrongETH = fee - 1; // off by 1 wei
vm.prank(user);
snow.buySnow{value: wrongETH}(1);
// Step 2: User lost ETH (stuck in contract)
assertEq(address(snow).balance, wrongETH);
// Step 3: User ALSO paid WETH
assertEq(weth.balanceOf(user), userWETHBefore - fee);
// Step 4: User paid double — ETH + WETH — for 1 Snow token
assertEq(snow.balanceOf(user), 1);
}

Recommended Mitigation

Restructure the if/else logic to explicitly check whether the user intends to pay with ETH or WETH. If msg.value > 0, verify it matches the exact fee and reject mismatched amounts with a revert. If msg.value == 0, proceed with the WETH payment path. This prevents users from accidentally sending ETH that doesn't match the fee, eliminating the double-payment scenario entirely.

function buySnow(uint256 amount) external payable canFarmSnow {
+ if (amount == 0) revert S__ZeroValue();
- 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);
- }
+ if (msg.value > 0) {
+ if (msg.value != (s_buyFee * amount)) revert S__IncorrectETHAmount();
+ _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);
}
Updates

Lead Judging Commences

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