Snowman Merkle Airdrop

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

Missing Per-User Rate Limiting in earnSnow() Allows Multi-User Reuse

Root + Impact

Description

  • The earnSnow() function in the Snow contract uses a global timestamp variable s_earnTimer to enforce a cooldown period between token mints. However, this timer is shared across all users. This allows any user to call earnSnow() and reset the timer, thereby preventing all other users from calling it until 1 week has passed.

  • This logic creates a denial-of-service condition where a malicious user can block others from earning snow tokens by invoking the function themselves at regular intervals.

//Affected Code:
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 High

Likelihood:

  • This occurs when the first user calls earnSnow()

  • The s_earnTimer variable is shared across all users, making the system vulnerable to griefing or accidental lockouts by a single address.

Impact:

  • Denial of service for other users

  • Breaks expected UX and what function should basically

Proof of Concept: Below is a test case to demonstrate the issue using Foundry:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/Snow.sol";
contract SnowTest is Test {
Snow public snow;
address public user1 = vm.addr(1);
address public user2 = vm.addr(2);
function setUp() public {
snow = new Snow(address(0x1234), 1, address(0x5678));
vm.warp(block.timestamp); // set block.timestamp
}
function test_GlobalTimerBlocksOtherUsers() public {
vm.startPrank(user1);
snow.earnSnow(); // user1 calls successfully
vm.stopPrank();
vm.startPrank(user2);
vm.expectRevert(); // should revert due to global timer
snow.earnSnow(); // user2 gets blocked
vm.stopPrank();
}
}

Recommended Mitigation : Use a per user mapping to store cooldowns individually:

- remove this code
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;
}
+ add this code
mapping(address => uint256) private s_lastEarned;
function earnSnow() external canFarmSnow {
if (s_lastEarned[msg.sender] != 0 && block.timestamp < (s_lastEarned[msg.sender] + 1 weeks)) {
revert S__Timer();
}
_mint(msg.sender, 1);
s_lastEarned[msg.sender] = block.timestamp;
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

social_retard Submitter
3 months ago
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.