Snowman Merkle Airdrop

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

## Snow.sol ## [ Users Blocked from earnSnow ]

Root + Impact

Description

Currently, a single global variable s_earnTimer is used to track the last snow-related activity. This causes unintended side effects because:

  • Every time any user calls buySnow(), s_earnTimer is updated globally.

  • This interferes with the logic in earnSnow(), which uses s_earnTimer to enforce a 1-week cooldown per user.

  • As a result, one user's buySnow() call can block another user's ability to earnSnow()

// buySnow() resets global timer for everyone
@> s_earnTimer = block.timestamp;
// earnSnow() depends on global timer
@> if (block.timestamp < s_earnTimer + 1 weeks) {
revert S__Timer();
}

Risk

Likelihood:

High

  • This will occur every time multiple users interact with buySnow() and earnSnow().

  • Particularly problematic on live systems with many users, or bots triggering buys rapidly.


Impact:

The use of a global timer:

  • Breaks user-isolated reward logic.

  • Allows one user to unintentionally or maliciously delay others from earning rewards.

  • Prevents accurate tracking of per-user activity for cooldown enforcement, loyalty rewards, or future reward scaling.

Proof of Concept

Example of the Problem:

Alice calls buySnow() → s_earnTimer = block.timestamp;
Bob calls earnSnow() → allowed (first time);
Charlie calls buySnow() → s_earnTimer is updated;
Bob calls earnSnow() again → reverts due to S__Timer.

Any user's buySnow() delays every user's ability to earnSnow() again.

Recommended Mitigation

Replace the single global s_earnTimer with per-user mappings and update both functions buySnow() and earnSnow().

mapping(address => uint256) public snowPurchased;
mapping(address => uint256) public lastEarnTime;
// Updated buySnow()
function buySnow(uint256 amount) external payable canFarmSnow {
uint256 totalCost = s_buyFee * amount
if (msg.value == (totalCost)) {
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), totalCost);
_mint(msg.sender, amount);
}
// Track purchases
snowPurchased[msg.sender] += amount;
// Per-user timer
lastEarnTime[msg.sender] = block.timestamp;
emit SnowBought(msg.sender, amount);
}
// Updated earnSnow()
function earnSnow() external canFarmSnow {
if (lastEarnTime[msg.sender] != 0 && block.timestamp < (lastEarnTime[msg.sender] + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
lastEarnTime[msg.sender] = block.timestamp;
emit SnowEarned(msg.sender, 1);
}
- s_earnTimer = block.timestamp;
+ mapping(address => uint256) public snowPurchased;
+ mapping(address => uint256) public lastEarnTime;
Updates

Lead Judging Commences

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