Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: high
Likelihood: high

Global Timer Reset in `Snow::earnSnow` Disables Free Weekly Claims for All Users

Author Revealed upon completion

Root + Impact

Description

The Snow::earnSnow() function uses a single global timer (s_earnTimer) to enforce its "once per week" claim restriction. When any user claims free tokens:

  • The global timer resets to the current timestamp

-All subsequent claims (by any user) are blocked for 7 days
This contradicts protocol documentation stating users can claim tokens "for free once a week" (per-account basis).

Impact:

  • Complete Denial-of-Service: Free claims become unusable

  • Broken Protocol Incentives: Renders core user acquisition feature nonfunctional

  • Economic Damage: Eliminates promised free token distribution

  • Reputation Harm: Users perceive protocol as dishonest

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;
}

Risk

Likelihood:

• Triggered by normal protocol usage
• Requires only one claim per week to maintain blockage
• Exploitable with minimal gas costs

Impact:

• Permanent suppression of core feature
• Loss of user trust and adoption
• Violates documented tokenomics

Proof of Concept

// Day 0: First claim (always allowed)
snow.earnSnow(); // s_earnTimer = T0
// Day 1: UserA claims -> BLOCKS ALL USERS
snow.earnSnow(); // s_earnTimer = T1
// Day 2: UserB attempts claim -> REVERT
// Check: block.timestamp (T2) < T1 + 1 week
snow.earnSnow(); // ❌ Reverts with S__Timer()
// Day 8: UserB retries -> STILL BLOCKED
// (if any claim occurred between T1 and T8)

Attack Vector:

  1. Malicious actor claims at T0

  2. Claims again at T0 + 6 days 23 hours

  3. All users blocked until T0 + 13 days 23 hours

  4. Repeat weekly → permanent suppression

Recommended Mitigation

// STEP 1: Replace global timer with per-user mapping
mapping(address => uint256) private s_lastClaim;
// STEP 2: Modify earnSnow()
function earnSnow() external canFarmSnow {
// Per-user check
if (s_lastClaim[msg.sender] != 0 &&
block.timestamp < s_lastClaim[msg.sender] + 1 weeks
) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_lastClaim[msg.sender] = block.timestamp; // Update user-specific timer
emit SnowEarned(msg.sender, 1); // Add missing event
}
// STEP 3: Remove s_earnTimer state variable

Initialization:

constructor(...) {
s_lastClaim[address(0)] = 1; // Prevent zero-time edge case
}

Documentation Alignment:
Update specs to clarify: "Each address can claim free tokens once per 7-day period."

Event Emission:
Ensure SnowEarned is emitted for on-chain transparency

Support

FAQs

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