Snowman Merkle Airdrop

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

[H-04] Global Earn-Timer DoS

Global Earn-Timer DoS

Description

  • Normal behaviour: Each user should wait a week between successive earnSnow calls.

  • Issue: A single global variable s_earnTimer throttles all users; the first caller blocks everyone for one week.

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

  • Soon after launch, one address will earn, automatically locking out the rest.

  • Sybil accounts can grief the system indefinitely.

Impact

  • Honest users cannot farm tokens; feature becomes unusable.

  • Centralisation: first-mover enjoys unfair advantage.

Proof of Concept

test/PoC_Snow_GlobalTimerDoS.t.sol

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {DeploySnow} from "../script/DeploySnow.s.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
/**
* @title PoC_Snow_GlobalTimerDoS
* @notice Shows that `earnSnow()` uses a SINGLE global timestamp (`s_earnTimer`),
* letting the first caller block everyone else from earning for an entire
* week, a denial-of-service bug.
*/
contract PoC_Snow_GlobalTimerDoS is Test {
Snow snow;
MockWETH weth;
address alice;
address bob;
function setUp() public {
DeploySnow deployer = new DeploySnow();
snow = deployer.run();
weth = deployer.weth();
alice = makeAddr("alice");
bob = makeAddr("bob");
}
function testFirstEarnerBlocksOthers() public {
// Alice earns 1 token — succeeds
vm.prank(alice);
snow.earnSnow();
assertEq(snow.balanceOf(alice), 1);
// Bob tries to earn immediately — reverts with S__Timer
vm.prank(bob);
vm.expectRevert(Snow.S__Timer.selector);
snow.earnSnow();
// After a week passes, Bob can finally earn
vm.warp(block.timestamp + 1 weeks);
vm.prank(bob);
snow.earnSnow();
assertEq(snow.balanceOf(bob), 1);
}
}

Recommended Mitigation

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

Lead Judging Commences

yeahchibyke Lead Judge
12 days ago
yeahchibyke Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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