Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Valid

Global Lockout Vulnerability by a Single User, Blocking All Others from Earning Snow for 1 Week

Global Snow.sol::s_earnTimer state variable Restricts All Users From Earning Snow Individually

Description

  • Users should be able to earn or buy SNOW tokens freely, with no restrictions from other user's actions

  • The contract uses a single shared timer (s_earnTimer). If one user earns or buys SNOW everyone else is blocked from earning for a full week

function buySnow(uint256 amount) external payable canFarmSnow {
if (msg.value == (s_buyFee * amount)) {
_mint(msg.sender, amount);
} else {
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
_mint(msg.sender, amount);
}
@> s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
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:

  • Users frequently interact : Buying and earning SNOW are core functions, meaning the time resets often

  • NO restrictions on who triggers it: Any user (even accidentally) can lock others out by simply using the contract normally

Impact:

  • Unfair User Experience : Legitimate users are blocked from earning SNOW tokens simply because another user interacted with the contract first. This creates frustration and discourages participation

  • System Abuse Potential : A malicious actor can intentionally trigger the timer repeatedly effectively freezing SNOW earnings for all other users and disrupting the intended token distribution

Proof of Concept


This test demonstrates how the global s_earnTimer unfairly blocks all users when just one interacts with the contract.

function testGlobalTimerBlocksAllUsers() public {
// Test that one user's action blocks others
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
vm.deal(user1, FEE*5);
vm.deal(user2, FEE*5);
vm.deal(user3, FEE*5);
vm.prank(user1);
snow.buySnow{value: FEE}(1);
// Verify user2 cannot earnSnow right after user1
vm.prank(user2);
vm.expectRevert(); // Should revert with S__Timer
snow.earnSnow();
// Verify user3 also cannot earnSnow
vm.prank(user3);
vm.expectRevert();
snow.earnSnow();
// Warp exactly 1 week later
vm.warp(block.timestamp + 1 weeks);
// Now user2 should succeed
vm.prank(user2);
snow.earnSnow();
assertEq(snow.balanceOf(user2), 1, "user2 should get 1 SNOW");
// But user3 is now blocked again
vm.prank(user3);
vm.expectRevert();
snow.earnSnow();
// Verify user1 can buy again after another week
vm.warp(block.timestamp + 1 weeks);
vm.prank(user1);
snow.buySnow{value: FEE}(1);
assertEq(snow.balanceOf(user1), 2, "user1 should accumulate SNOW");
}

Recommended Mitigation

  1. Replaced global s_earnTimer with mapping(address => uint256) private s_earnTimer;

  2. **Updated Functions ** to use caller-specific timers:

  • buySnow(): Sets timer only for msg.sender

  • earnSnow(): Checks/updates only the caller's timer

- uint256 private s_earnTimer;
+ mapping(address => uint256) private s_earnTimer;
.
.
.
.
function buySnow(uint256 amount) external payable canFarmSnow {
/*
if-else condition to receive eth or weth based on msg.value
*/
- s_earnTimer = block.timestamp;
+ s_earnTimer[msg.sender] = block.timestamp;
emit SnowBought(msg.sender, amount);
}
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[msg.sender] = block.timestamp;
}
}
Updates

Lead Judging Commences

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

Appeal created

mohankrishkotte Submitter
5 months ago
yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

buying of snow resets global timer thus affecting earning of free snow

When buySnow is successfully called, the global timer is reset. This inadvertently affects the earning of snow as that particular action also depends on the global timer.

Support

FAQs

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