Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Valid

[L-2] `buySnow` Allows Zero Amount Purchases

Root + Impact

  • Root: The buySnow function accepts amount = 0 without reverting, proceeding with state updates and events;

  • Impact: Wastes gas and allows unintended state manipulation without token minting.

Description

  • The buySnow function processes amount = 0, triggering _mint(msg.sender, 0)or a zero WETH transfer, updating s_earnTimer and emitting SnowBought without minting tokens.

  • This violates the intent of a purchase function, leading to unnecessary gas costs and potential abuse.

// Root cause in the codebase with @> marks to highlight the relevant section// Root cause in the codebase with @> marks to highlight the relevant section
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:

  • When a user or attacker calls buySnow(0) within the farming period.

  • During normal operation with invalid inputs.

Impact:

  • Incurs gas costs without value (low).

  • Potential state manipulation (e.g., s_earnTimer) for abuse (medium).

Proof of Concept

Zero Amount Test: Calling buySnow(0) succeeds without reverting.

function testBuySnowZeroAmount() public {
uint256 farmingEnd = snow.i_farmingOver();
vm.warp(farmingEnd - 1); // Within period
vm.startPrank(jerry);
uint256 buyFee = snow.s_buyFee();
weth.mint(jerry, buyFee);
weth.approve(address(snow), buyFee);
vm.expectRevert(); // Expect revert, but passes
snow.buySnow(0);
vm.stopPrank();
uint256 balanceJerry = snow.balanceOf(jerry);
console2.log("Jerry's tokens after zero mint:", balanceJerry);
console2.log("Earn timer updated:", snow.s_earnTimer() > 0);
}

And when we hide the expectEmit line for it to pass and run forge test --mt testBuySnowZeroAmount -vv we get:

Ran 1 test for test/TestSnow.t.sol:TestSnow
[PASS] testBuySnowZeroAmount() (gas: 94274)
Logs:
Jerry's tokens after zero mint: 0
Earn timer updated: true
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.07ms (100.39µs CPU time)
  • Result: Jerry's tokens after zero mint: 0, Earn timer updated: true, showing state change without minting.

Recommended Mitigation

function buySnow(uint256 amount) external payable canFarmSnow {
+ if (amount == 0) {
+ revert S__InvalidAmount();
+ }
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);
}
  • Add a check to revert for amount = 0.

Updates

Lead Judging Commences

yeahchibyke Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

buying of snow resets global timer thus affecting earning of free snow

When buySnow is successfully called, the global timer is reset. This inadvertently affects the earning of snow as that particular action also depends on the global timer.

Support

FAQs

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