Snowman Merkle Airdrop

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

Global Timer in `earnSnow()` Enables Frontrunning and Unfair Reward Distribution

Root + Impact

Description

The earnSnow() function is designed to allow users to mint 1 SNOW token once every week. It uses a global timer s_earnTimer to enforce the cooldown period.

However, the use of a global cooldown across all users introduces a first-come-first-serve race condition, where the first user to call the function after the cooldown period can claim the reward, while all others will revert. This creates a frontrunning vector, enabling malicious actors or bots to consistently claim the weekly reward.

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 every time the cooldown period expires, as the function is globally accessible to anyone.

  • Users monitoring the blockchain can predict when the timer resets and preemptively send high-gas transactions.

Impact:

  • Only one user can successfully mint the SNOW token per week, leading to unfair distribution.

  • Honest users may waste gas fees on reverted transactions, resulting in economic loss and frustration.

Proof of Concept

// Assume Alice and Bob are watching the contract
// 1. Alice calls earnSnow() after cooldown — it succeeds
earnSnow(); // ✅ Mints 1 SNOW to Alice
// 2. Bob tries to call right after in the same block
earnSnow(); // ❌ Reverts with S__Timer()
// MEV bots can exploit this pattern by frontrunning or sending high-gas txs to monopolize the reward

Recommended Mitigation

Use a per-user cooldown mechanism instead of a global timer:

- if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
- revert S__Timer();
- }
- s_earnTimer = block.timestamp;
+ if (lastEarn[msg.sender] != 0 && block.timestamp < lastEarn[msg.sender] + 1 weeks) {
+ revert S__Timer();
+ }
+ lastEarn[msg.sender] = block.timestamp;

Additionally, declare:

mapping(address => uint256) public lastEarn;

This ensures fair distribution and prevents frontrunning.

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.