Snowman Merkle Airdrop

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

Attacker can prevent all users from earning free tokens with global timer manipulation

A single attacker can permanently prevent all users from earning free tokens due to global timer manipulation in Snow::buySnow and Snow::earnSnow

Description: The Snow contract uses a single global timer s_earnTimer that is reset whenever any user calls buySnow() or earnSnow(). This allows a malicious actor to continuously reset the timer, preventing all other users from ever claiming their weekly free tokens.

uint256 private s_earnTimer;
function buySnow(uint256 amount) external payable canFarmSnow {
// ... payment logic ...
s_earnTimer = block.timestamp; // ❌ Resets timer for ALL users
}
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; // ❌ Resets timer for ALL users
}

An attacker can exploit this by:

  1. Waiting until legitimate users are about to claim (6 days + 23 hours)

  2. Calling buySnow() to reset the global timer

  3. Repeating this process indefinitely to maintain permanent DoS

Impact: Complete denial of service for free token distribution, breaking core protocol functionality.

Proof of Concept:

function test_GlobalTimerManipulation() public {
// Victim earns their first free token
vm.prank(victim);
snow.earnSnow();
// Fast forward almost 1 week (6 days, 23 hours)
vm.warp(block.timestamp + 6 days + 23 hours);
// Attacker buys snow, resetting the GLOBAL timer
vm.prank(attacker);
snow.buySnow{value: getBuyFee()}(1);
// Now victim cannot earn - timer was reset!
vm.prank(victim);
vm.expectRevert();
snow.earnSnow(); // This fails due to timer reset
// Attacker can repeat this every 6 days to permanently block free earnings
vm.warp(block.timestamp + 6 days + 23 hours);
vm.prank(attacker);
snow.buySnow{value: getBuyFee()}(1); // Reset timer again
vm.prank(victim);
vm.expectRevert();
snow.earnSnow(); // Still blocked!
}

Recommended Mitigation: Replace global timer with per-user timers:

mapping(address => uint256) private s_earnTimers; // ✅ Per-user timers
function buySnow(uint256 amount) external payable canFarmSnow {
// ... payment logic ...
// Remove global timer manipulation - buying doesn't affect earning timer
}
function earnSnow() external canFarmSnow {
uint256 userLastEarn = s_earnTimers[msg.sender];
if (userLastEarn != 0 && block.timestamp < (userLastEarn + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_earnTimers[msg.sender] = block.timestamp; // ✅ Only update caller's timer
}
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.