Snowman Merkle Airdrop

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

Global s_earnTimer causes permanent Denial of Service for free Snow farming

Root + Impact

Description

Normal behavior dictates that each user should have their own 1-week cooldown between using the earnSnow() function to mint 1 Snow token for free.

However, the restriction is enforced by a single global state variable s_earnTimer. Once any user calls it, the timer is updated for everyone in the protocol.

@> uint256 private s_earnTimer;
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:

  • This will occur the moment any single user in the protocol legitimately calls earnSnow(), updating the global timer and locking out everyone else.

Impact:

  • The protocol suffers from a persistent Denial of Service (DoS) for farming free Snow tokens, as only 1 user across the entire blockchain can earn snow per week.

Proof of Concept

This proof of concept demonstrates how the global timer blocks all other users. It begins by having an innocent user (innocent1) successfully call earnSnow(), claiming their 1 Snow token. Immediately after, a completely different user (innocent2) attempts to call the same function. Because s_earnTimer was updated globally by the first user rather than specifically for their own address, the second user's legitimate transaction reverts with S__Timer(), locking them—and everyone else—out of the yield for the next week.

function test_EarnSnowGlobalDoS() public {
// Since deployer.run() already warped time and called earnSnow() for 'eli',
// s_earnTimer is currently block.timestamp. Let's warp time beyond 1 week
// so innocent1 can legitimately call it, proving they would have succeeded.
vm.warp(block.timestamp + 1 weeks + 1 seconds);
// Innocent1 earns snow
vm.prank(innocent1);
snow.earnSnow();
assertEq(snow.balanceOf(innocent1), 1);
// Innocent2 tries to earn snow immediately after
vm.prank(innocent2);
vm.expectRevert(); // This will revert because the timer is global
snow.earnSnow();
console2.log("Innocent2 cannot earn snow because Innocent1 already invoked it!");
}

Recommended Mitigation

Recommended Mitigation: Use a mapping(address => uint256) to track the timestamp of the last claim for each individual user instead of a single global variable.

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

Lead Judging Commences

ai-first-flight-judge Lead Judge 4 days 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!