Snowman Merkle Airdrop

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

Global timer allows griefing attack

Root + Impact

The Snow.sol contract uses a single global timer variable, s_earnTimer , which is reset every time any user calls the buySnow function. This allows a criminal to perform a cheap continuous griefing attack that causes a DoS on the earnSnow function, preventing all other users from ever earning tokens.

Description

  • The intended behavior is for a user to be able to claim one free S token by calling earnSnow one week after their last purchase or their last claim. This mechanism is meant to be a primary way for users to acquire tokens over time.

  • The issue is that the contract uses a single s_earnTimer state variable to manage the earning cooldown for all users. This timer gets overwritten by any call to buySnow. An attacker can repeatedly call buySnow with a minimal amount just before the one week cooldown expires, perpetually resetting the timer and trapping all other users in a state where they can never successfully call earnSnow

// @> The single global timer variable.
uint256 private s_earnTimer;
function buySnow(uint256 amount) external payable canFarmSnow {
// ... logic ...
// @> The timer is reset globally on any purchase.
s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
function earnSnow() external canFarmSnow {
// @> The check relies on the global timer, which can be manipulated by others.
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_earnTimer = block.timestamp;
}

Risk

Likelihood:

  • An attacker calls the buySnow function with a minimal amount just before the one-week cooldown period is set to expire.

  • This attack is inexpensive and can be easily automated with a script to ensure the earnSnow function is permanently blocked for others.

Impact:

  • A core feature of the protocol is disabled for all legitimate users. This denies them access to one of the two intended methods of acquiring Snow tokens.

  • The attack degrades the user experience and undermines the token's distribution model, potentially causing users to lose faith in the project.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./Snow.sol";
contract DoSAttackOnEarning {
Snow public snowContract;
constructor(address _snow) {
snowContract = Snow(_snow);
}
function perpetualDoSAttack() external {
// This function should be called every week by the attacker
// Step 1: Check we can earn (timer should be reset)
// If this reverts with S__Timer(), wait until it doesn't
// Step 2: Earn our weekly Snow token
snowContract.earnSnow(); // Resets global timer
// Step 3: All other users are now blocked for another week
// Legitimate users calling earnSnow() will revert with S__Timer()
}
function demonstrateDoS() external {
// Week 1
snowContract.earnSnow(); // Attacker earns, timer = block.timestamp
// Simulate 6 days passing
vm.warp(block.timestamp + 6 days);
// Legitimate user tries to earn after 6 days - should fail
vm.prank(address(0x456)); // Simulate different user
try snowContract.earnSnow() {
revert("Should have failed!");
} catch {
// Expected - timer not expired yet
}
// Week 2 - exactly 7 days later
vm.warp(block.timestamp + 1 days); // Total: 7 days
// Attacker earns again before anyone else can
snowContract.earnSnow(); // Resets timer again
// Now legitimate users must wait another full week
}
}
// Attack scenario:
// 1. Attacker deploys contract and calls earnSnow() every 7 days
// 2. Global timer constantly resets, preventing other users from earning
// 3. Attacker accumulates Snow tokens while others are blocked
// 4. Other users forced to buy Snow with ETH/WETH instead of earning free

Recommended Mitigation

The earning cooldown timer should be tracked on a per-user basis. This can be achieved by changing s_earnTimer from a single uint256 to a mapping(address => uint256). This ensures that one user's activity cannot affect another user's ability to earn tokens.

- uint256 private s_earnTimer;
+ mapping(address => uint256) private s_userEarnTimers;
function earnSnow() external canFarmSnow {
- if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
+ if (s_userEarnTimers[msg.sender] != 0 && block.timestamp < (s_userEarnTimers[msg.sender] + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
- s_earnTimer = block.timestamp;
+ s_userEarnTimers[msg.sender] = block.timestamp;
}
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.