Snowman Merkle Airdrop

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

Logic Flaw in buySnow() Allows ETH Loss and Improper Payment

Root + Impact

Description

  • Describe the normal behavior in one or more sentences
    The `buySnow()` function has flawed conditional logic that can result in loss of ETH, improper payment handling, and allows users to pay with both ETH and WETH simultaneously without proper validation.

  • Explain the specific issue or problem in one or more sentence

    The function uses an `if-else` structure that doesn't properly handle edge cases. If `msg.value` doesn't exactly equal the required fee, it attempts to transfer WETH, even if ETH was sent. Additionally, excess ETH is not refunded.

```solidity
// @> Snow.sol:79-90
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);
}
```
**Issues:**
1. If user sends ETH but `msg.value != (s_buyFee * amount)`, the function still tries to transfer WETH and keeps the ETH
2. If user sends excess ETH (`msg.value > (s_buyFee * amount)`), excess is not refunded
3. If user sends both ETH and approves WETH, both payments could be taken
4. No validation that payment was actually received

Risk

Likelihood:

  • * Users may accidentally send incorrect amounts of ETH

    * Front-end or integration bugs could send wrong values

    * Users might send ETH when they intended to pay with WETH or vice versa

    * Occurs whenever `msg.value` doesn't exactly match the required fee

Impact:

  • * Permanent loss of user funds (ETH sent but not used)

    * Users paying twice (both ETH and WETH)

    * Contract accumulating ETH that should have been refunded

    * Poor user experience and potential financial losses

Proof of Concept

```solidity
function testBuySnowEthLoss() public {
Snow snow = deploySnow();
address user = makeAddr("user");
uint256 fee = snow.s_buyFee();
// User sends excess ETH
vm.deal(user, fee * 2);
vm.prank(user);
snow.buySnow{value: fee * 2}(1); // Sends 2x the required fee
// User only gets 1 token, but paid 2x
assert(snow.balanceOf(user) == 1);
// Excess ETH is lost in the contract
assert(address(snow).balance == fee * 2); // Should only be fee
// User sends less ETH but has WETH approved
vm.deal(user, fee / 2);
weth.mint(user, fee);
vm.startPrank(user);
weth.approve(address(snow), fee);
snow.buySnow{value: fee / 2}(1); // Sends ETH but function uses WETH
vm.stopPrank();
// User paid both ETH and WETH
assert(address(snow).balance > 0); // ETH was sent
assert(weth.balanceOf(address(snow)) == fee); // WETH was also transferred
}
```

Recommended Mitigation

```diff
// Snow.sol
function buySnow(uint256 amount) external payable canFarmSnow {
uint256 requiredFee = s_buyFee * amount;
+ if (msg.value > 0) {
+ // Paying with ETH
+ if (msg.value != requiredFee) {
+ revert S__InvalidPaymentAmount();
+ }
+ _mint(msg.sender, amount);
+ } else {
+ // Paying with WETH
+ i_weth.safeTransferFrom(msg.sender, address(this), requiredFee);
+ _mint(msg.sender, amount);
+ }
- 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);
}
```
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 17 days 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!