Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: low
Likelihood: high
Invalid

Snow::s_earnTimer is a single global timer, letting one caller grief-DoS weekly earnSnow for all users

Description

Snow lets users earn 1 free Snow per week via earnSnow, rate-limited by a timer. But the timer is a single global storage variable, not a per-user mapping:

uint256 private s_earnTimer; // @> one global timer shared by 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; // @> any caller resets it for EVERYONE
}

buySnow also writes s_earnTimer = block.timestamp. Because the timer is shared, the moment any address calls earnSnow (or buySnow), the one-week lockout applies to all users, regardless of whether a given user has ever earned.

Risk

Likelihood: High — triggering it is trivial and permissionless: a single earnSnow call per week by any one address keeps the timer perpetually fresh. It is also hit accidentally in normal use (any active user blocks all others).

Impact: Low — this is a griefing / availability issue on the free-earn faucet. No funds are lost and the lock auto-clears after a week; users can still obtain Snow via buySnow.

Impact

One address can indefinitely deny every other user the ability to earn free Snow: by calling earnSnow once per week, the global timer never lets anyone else past the S__Timer check. The weekly free-earn feature is effectively DoS'd for the whole user base.

Proof of Concept

The following Foundry test passes. Alice earns, which sets the global timer; Bob — who has never earned — is then blocked within the same week:

function test_F3_globalEarnTimerGriefsAll() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
// Alice earns -> sets the GLOBAL timer to now.
vm.prank(alice);
snow.earnSnow();
// Bob tries to earn in the same week -> reverts, even though Bob never earned.
vm.prank(bob);
vm.expectRevert(); // S__Timer()
snow.earnSnow();
// After a week passes, it opens again (for whoever calls first).
vm.warp(block.timestamp + 7 days + 1);
vm.prank(bob);
snow.earnSnow();
assertEq(snow.balanceOf(bob), 1);
}

Recommended Mitigation

Track the timer per user:

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

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!