Snowman Merkle Airdrop

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

Global Earn Timer

Root + Impact

Description

  • The intended behavior is that each user can earn 1 Snow token once every 1 week, individually tracked per address.

  • The actual implementation uses a single global timestamp (s_earnTimer) to enforce the cooldown, which means that when one user earns Snow, all other users are blocked from earning for one week. This allows any user to effectively block others from using the earnSnow() function by calling it just before others attempt to claim.

// Root cause in the codebase with @> marks to highlight the relevant section
function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
@> revert S__Timer(); // ❌ Shared global timer restricts all users
}
_mint(msg.sender, 1);
@> s_earnTimer = block.timestamp; // ❌ Global update blocks everyone else
}

Risk

Likelihood:

  • A user will call earnSnow() as soon as the 1-week timer expires

  • Other users attempting to earn within that week will be reverted due to the shared cooldown

Impact:

  • Any user can grief the system and block others from earning

  • Honest users are unable to claim their Snow fairly and individually

  • Creates unfair access to token rewards, violating per-user expectations

Proof of Concept

Once UserA calls earnSnow(), the cooldown affects all addresses, not just the one that triggered it. As a result, UserB is blocked, even though they haven't called earnSnow() previously.

// Assume UserA and UserB both want to earn Snow
// UserA calls earnSnow()
snow.earnSnow(); // succeeds
// Immediately after, UserB tries:
snow.connect(userB).earnSnow();
// ❌ Reverts with S__Timer, even though UserB hasn't earned yet

Recommended Mitigation

The mitigation introduces a per-user cooldown timer by switching from a single uint256 to a mapping(address => uint256). This ensures that each user has an independent earning window and cannot affect other users’ ability to earn Snow. The logic now accurately tracks each user's most recent claim and enforces the 1-week delay only for them.

- 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;
emit SnowEarned(msg.sender, 1);
}
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.