Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

H02-Risk of overpaying fees for the users with buySnow

Root + Impact

Description

The buySnow function allows users to purchase Snow tokens by either sending ETH directly or paying with WETH. The function attempts to determine the payment method based on whether the msg.value exactly matches the required ETH fee (s_buyFee * amount). If not, it falls back to attempting a WETH safeTransferFrom.

As the result, the functionbuySnow function can take more fees than intended due to imprecise ETH value matching and silent fallback to WETH.

The specific issue is that this logic is ambiguous and unsafe. Users who mistakenly overpay or underpay by even 1 wei will silently trigger a WETH transfer, which may fail if WETH has not been approved. This causes unexpected reverts, poor UX, and the most severe this introduces financial risk for users and undermines trust in the token mechanics.

Additionally, there's no way for the user to explicitly choose the payment method, leading to unpredictable behavior.

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

Risk

Likelihood:

  • This occurs whenever a user provides an incorrect ETH value (overpaying or underpaying by any amount), which is common due to frontend inconsistencies or slippage buffers.

  • This also occurs when a user unknowingly relies on WETH payment without approving the correct allowance beforehand.

Impact:

  • Users experience failed transactions and wasted gas due to unintuitive fallback behavior.

  • financial risk for users and undermines trust in the token mechanics.

Proof of Concept

Add the following test in TestSnow

Here the contract will take WETH as fees + the value of msg.value (here 1 wei). As a result, it will collects more fees as intended.

function testCanBuySnowWithWEthAndContractTakeAlsoMsgValue() public {
assert(jerry.balance == 0);
vm.deal(jerry, 1 wei);
// In the past, the sender has approved the Snow contract
vm.startPrank(jerry);
weth.approve(address(snow), FEE);
// Now he want to pay with ETH but the amount is too low
// As a result the contract takes the weth amount and msg.value
snow.buySnow{value: 1 wei}(1);
vm.stopPrank();
assert(weth.balanceOf(address(snow)) == FEE);
assert(snow.balanceOf(jerry) == 1);
assert(address(snow).balance == 1 wei);
assert(jerry.balance == 0);
}

Recommended Mitigation

Separate the two different methods of payment in two functions.

Ensures clear and intentional user behavior, removes ambiguity, and prevents unintentional failures or misuse.

- 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);
- }
+error InvalidETHAmount();
+ function buySnowWithETH(uint256 amount) external payable canFarmSnow {
+ uint256 cost = s_buyFee * amount;
+ if (msg.value !!= cost) revert InvalidETHAmount();
+ _mint(msg.sender, amount);
+ emit SnowBought(msg.sender, amount);
+ }
+ function buySnowWithWETH(uint256 amount) external canFarmSnow {
+ uint256 cost = s_buyFee * amount;
+ i_weth.safeTransferFrom(msg.sender, address(this), cost);
+ _mint(msg.sender, amount);
+ emit SnowBought(msg.sender, amount);
+ }
Updates

Lead Judging Commences

yeahchibyke Lead Judge 10 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.