Snowman Merkle Airdrop

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

Users Can Lose Funds by Accidentally Paying with Both ETH and WETH

buySnow() logic incorrectly assumes a WETH payment when an incorrect amount of ETH is sent, causing a user to be charged in both assets and leading to the permanent loss of their initial ETH deposit.

Description

  • The buySnow() function allows users to purchase "Snow" tokens by paying a fee. It is designed to accept payment in either native ETH (via msg.value) or WETH (via safeTransferFrom).

  • The issue arises from the flawed if/else logic that determines the payment method. The code only accepts an ETH payment if msg.value is exactly equal to the required fee. If a user sends any other non-zero amount of ETH (e.g., one wei less or one wei more than required), the else block is executed. This block proceeds to charge the user the full fee in WETH, assuming that was the user's intent. The critical flaw is that the native ETH sent in msg.value is accepted by the payable function but is never refunded or accounted for, resulting in the user paying with both ETH and WETH for a single purchase.

function buySnow(uint256 amount) external payable canFarmSnow {
@> if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
@> } else {
// If msg.value is > 0 but not the exact fee, the ETH is kept by the contract...
@> i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
// ...and the user is also charged the full amount in WETH.
_mint(msg.sender, amount);
}
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}

Risk

Likelihood: Medium

  • This occurs any time a user attempts to pay with native ETH but provides an incorrect msg.value. This can easily happen due to user error, a front-end UI bug, or transaction misconfiguration.

  • The vulnerability requires the user to have also pre-approved the contract to spend their WETH, which is a common requirement in DeFi protocols.

Impact: High

  • Direct Loss of User Funds: Users who trigger this condition will lose the entire amount of ETH they sent in msg.value. The loss is permanent and irrecoverable.

Proof of Concept

The following test demonstrates that a user attempting to buy 2 "Snow" token by WETH, but accidentally sending ETH, will lose that ETH and also be charged the full WETH.

function testCanBuySnowWithWrongETH() public {
address alice = makeAddr("alice");
weth.mint(alice, FEE * 2);
deal(alice, FEE * 2);
vm.startPrank(alice);
weth.approve(address(snow), FEE * 2);
snow.buySnow{value: FEE}(2); // paying wrong ETH
vm.stopPrank();
assert(alice.balance == FEE); // alice lost half of ETH
assert(snow.balanceOf(alice) == 2); // alice still got two snow
assert(weth.balanceOf(alice) == 0); // alice still pay weth
}

Recommended Mitigation

The payment logic should be strict and unambiguous. Do not allow a transaction to fall through from an ETH payment attempt to a WETH payment. Explicitly separate the two payment flows. If msg.value is greater than zero, the function should treat it as an ETH-only payment attempt and revert if the amount is incorrect. WETH payments should only be processed when msg.value is zero.

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);
- }
+ uint256 requiredFee = s_buyFee * amount;
+
+ // Path 1: Payment with native ETH
+ if (msg.value > 0) {
+ require(msg.value == requiredFee, "Incorrect ETH amount sent for purchase");
+ _mint(msg.sender, amount);
+ // Path 2: Payment with WETH
+ } else {
+ i_weth.safeTransferFrom(msg.sender, address(this), requiredFee);
+ _mint(msg.sender, amount);
+ }
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
5 months ago
yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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