Snowman Merkle Airdrop

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

`Snow::buySnow` resets `s_earnTimer` preventing users from claiming free Snow tokens even after 1 week has passed.

Summary

The Snow::buySnow() function incorrectly resets the global s_earnTimer variable, which is used in earnSnow() to enforce a one-week cool down for free token claims. As a result, each time buySnow() is called, it resets the timer, preventing users from calling earnSnow() even though a week has not passed. This creates a Denial-of-service as it prevents earning free tokens.

Description

The variable s_earnTimer is used to enforce a 1-week period where free tokens can be claimed. This variable is correctly used in the earnSnow function and incorrectly used in the buySnow function.

The result of the incorrect use in buySnow is each time a user buys Snow tokens, the variable is reset with the current block timestamp which has the effect of setting this cdode in earnSnow to be true triggering a revert:

if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}

The result is, the s_earnTimer is always pushed forward to a time in the future ; preventing users from claiming free Snow tokens even though a "off-chain week" has technically elasped. This is a form of Denial of Service.

This can issue can be exploited:

  • intentionally by an attacker but will require investment

  • through normal use of the contract.

Affected Areas

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; //@audit: this should not be here. It breaks earnSnow()
emit SnowBought(msg.sender, amount);
}
function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_earnTimer = block.timestamp;
}

The severity of this issue is High for the following reasons:

  • Impact: High - It breaks a core functionality of the protocol whereby users are supposed to be able to earn free Snow tokens after a week has passed.

  • Likelihood: High - This issue will occur every time a user calls buySnow successfully; meaning the timer will always be pushed a week into the future.

Proof of Concept

As proof of the validity of this issue, I have created a runnable PoC to demonstrate the issue.

Description

  1. A user - Ashley - claims a free Snow token

  2. A week elapses allowing her to be able to claim another free Snow token.

  3. Before she can claim, another user - Victory - buys a snow token resetting the s_earnTimer to a time in the future.

  4. Ashley tries to claim but transaction reverts as the week period has been moved forward.

Code

Run with: forge test --mt testS_earnTimerResetAfterBuyingSnow

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "src/Snow.sol";
import {DeploySnow} from "script/DeploySnow.s.sol";
import {MockWETH} from "src/mock/MockWETH.sol";
contract TestSnow is Test {
Snow snow;
DeploySnow deployer;
MockWETH weth;
address collector;
uint256 FEE;
address jerry;
address victory;
address ashley;
function setUp() public {
deployer = new DeploySnow();
snow = deployer.run();
weth = deployer.weth();
collector = deployer.collector();
FEE = deployer.FEE();
jerry = makeAddr("jerry");
victory = makeAddr("victory");
ashley = makeAddr("ashley");
weth.mint(jerry, FEE);
deal(victory, 100 ether);
}
function testS_earnTimerResetAfterBuyingSnow() public {
//Ashley claims free token
vm.prank(ashley);
snow.earnSnow();
assertEq(snow.balanceOf(ashley), 1);
//Fast-forwading farming period
vm.warp(block.timestamp + 1 weeks + 1 days); //Should now be eligible to call earnSnow again
//Another User buys tokens
uint256 amountToBuy = 2;
vm.prank(victory);
snow.buySnow{value: amountToBuy * FEE}(amountToBuy);
assertEq(snow.balanceOf(victory), amountToBuy);
//Ashley cannot claim token again as timer is reset
vm.prank(ashley);
vm.expectRevert();
snow.earnSnow();
assertEq(snow.balanceOf(ashley), 1);
}
}

Mitigation

The recommended mitigation for this issue is to remove the line s_earner = block.timestamp in buySnow as it has no purpose. There are no protocol restrictions on buySnow.

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; //@audit: you dont need this. this breaks the `earnSnow` function
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 5 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.