Snowman Merkle Airdrop

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

Global timer causes a DOS

[H-02] Global Timer Prevents Independent Earnings

Description

The Snow protocol incorrectly uses a global cooldown timer (s_earnTimer) for earnings. This design flaw makes all users share the same timer, enabling griefing or denial-of-service (DoS) by a single user.

function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
} // <@ the s_earnTimer records only a single global state meaning all users share just 1 state
// @audit-low emit SnowEarned(msg.sender, 1) this should be included
_mint(msg.sender, 1);
s_earnTimer = block.timestamp; // <@ Once ashley claims for the week neither jerry nor victory can claim in that same week
}

Risk

Likelihood:

  • This will occur whenever more than one user attempts to earn Snow in the same 7-day period. Once one user triggers earnSnow(), others will find themselves blocked, regardless of when they last interacted

  • Inactive or malicious accounts can grief the protocol by calling earnSnow() once per week, locking out all other users from claiming rewards — effectively creating a low-cost denial of service.

Impact:

  • One user can block earnings for all others.

  • Users are incentivized to grief or race to block others.

  • Breaks UX and fairness.

Proof of Concept

Here, ashley earns 1 Snow and updates s_earnTimer. jerry is then blocked despite being a different user — highlighting the flaw in using a shared cooldown.

function testGlobalTimerBlocksFarmers() public {
vm.prank(ashley);
snow.earnSnow();
assertEq(1, snow.balanceOf(ashley));
vm.prank(jerry);
vm.expectRevert(); // Because s_earnTimer is global, not per-user
snow.earnSnow();
}

Recommended Mitigation

Replace the global s_earnTimer with a per-user cooldown using a mapping.

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+ contract Snow is ERC20, Ownable, ReentrancyGuard {
...
+ mapping(address => uint256) public s_lastClaimTime;
...
function earnSnow() external canFarmSnow noRentrant{
- if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
- revert S__Timer();
+ if (s_lastEarned[msg.sender] != 0 && block.timestamp < (s_lastEarned[msg.sender] + 1 weeks)) {
+ revert S__Timer();
_mint(msg.sender, 1);
+ SnowEarned(msg.sender, 1)
+ s_lastEarned[msg.sender] = block.timestamp;
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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