Snowman Merkle Airdrop

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

Mixed Payment Logic Enables Griefing & Unclear Purchase Path

Root + Impact

Description

Under normal conditions, users can mint Snow tokens by paying with ETH or WETH, and claim 1 free token every 7 days using earnSnow(). However, the contract uses a global timer (s_earnTimer) to enforce the cooldown, which is reset by anyone calling buySnow(). This allows a malicious user to repeatedly buy small amounts of Snow and grief the system—effectively blocking all others from claiming their weekly rewards..

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

This issue will occur whenever a user repeatedly calls the buySnow() function, which is designed to reset the shared s_earnTimer value.

  • Because there is no user-specific cooldown logic, any activity from a single user affects the entire protocol’s reward logic, making it trivial and cheap to abuse.

Impact:

All users can be indefinitely prevented from claiming their earnSnow() reward.

  • This undermines user trust, breaks the intended reward mechanism, and introduces a denial-of-service vulnerability at the protocol level.


Proof of Concept

This proof-of-concept demonstrates how a malicious user can exploit the global s_earnTimer by repeatedly calling buySnow() just before the 7-day cooldown expires. Each call resets the global timer, effectively blocking all other users from ever meeting the earnSnow() eligibility window.

The test simulates:

  1. An attacker buying Snow (which resets the timer).

  2. Advancing time close to the 7-day mark.

  3. The attacker buying again just before others can earn.

  4. A legitimate user attempting to call earnSnow() and being reverted due to the reset.

This behavior confirms that the cooldown mechanism is vulnerable to griefing attacks, where a single malicious actor can deny rewards to the entire user base.

Proof of Concept (Forge Test)

function testGriefingPreventsEarnSnow() public {
uint256 buyAmount = 1;
uint256 buyCost = snow.s_buyFee() * buyAmount;
// Step 1: Attacker buys snow, triggering s_earnTimer reset
vm.deal(attacker, buyCost);
vm.prank(attacker);
snow.buySnow{value: buyCost}(buyAmount);
// Step 2: Advance time close to 7 days
vm.warp(block.timestamp + 6 days + 23 hours);
// Step 3: Attacker buys again, resetting the global timer before cooldown ends
vm.prank(attacker);
snow.buySnow{value: buyCost}(buyAmount);
// Step 4: Honest user tries to call earnSnow() and gets reverted
vm.prank(user);
vm.expectRevert(S.S__Timer.selector);
snow.earnSnow();
}

Explanation

  • The attacker repeatedly interacts with buySnow() before the 7-day timer ends.

  • Since the timer is global, this denies all users from accessing the earnSnow() function.

  • This makes the protocol griefable — where any user can block rewards for others indefinitely and at minimal cost.

Recommended Mitigation

Problem

The current implementation uses a shared global timer (s_earnTimer) for throttling the earnSnow() reward. This allows any user to grief the entire protocol by constantly resetting the timer.

Solution

Replace the global timer with a per-user timer mapping, so that each user independently tracks their cooldown period.

Code Fix

Remove this global timer:

uint256 private s_earnTimer;

Replace with this per-user mapping:

mapping(address => uint256) private s_userEarnTimer;

Then update the earnSnow() function:

function earnSnow() external canFarmSnow {
if (block.timestamp < (s_userEarnTimer[msg.sender] + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_userEarnTimer[msg.sender] = block.timestamp;
}

Benefits

  • Eliminates griefing by ensuring each user's cooldown is isolated.

  • Preserves original intent of one reward per user every 7 days.

  • Gas-efficient, scalable, and improves fairness of the reward mechanism.


- remove this code
+ add this code
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.