Snowman Merkle Airdrop

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

Global Timer Affects All Users' Free Token Claims

Root + Impact

In file Snow.sol - The s_earnTimer is a global variable instead of a per-user mapping. When any user calls buySnow(), it resets the s_earnTimer to the current timestamp. This affects all users' ability to claim free tokens through earnSnow() since they share the same timer.

uint256 private s_earnTimer;
function buySnow(uint256 amount) external payable canFarmSnow {
// ... minting logic ...
s_earnTimer = block.timestamp; // Resets timer for all users
}
function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
// ... minting logic ...
}

Risk

Likelihood:

  • High as everytime a user trigger buySnow triggers a new s_earnTimer

Impact:

  • High as it breaking the invariant The Snow token can either be earned for free onece a week

Proof of Concept

  1. User A calls earnSnow() and gets a free token

  2. User B calls buySnow() which resets s_earnTimer

  3. User A cannot call earnSnow() again for another week, even though they should be able to

function testGlobalTimerAffectsAllUsers() public {
// Set up initial users
address userA = makeAddr("userA");
address userB = makeAddr("userB");
deal(userA, FEE);
deal(userB, FEE);
// UserA earnSnow and warp 1 week
vm.startPrank(userA);
snow.earnSnow();
assertEq(snow.balanceOf(userA), 1);
vm.warp(block.timestamp + 1 weeks);
vm.stopPrank();
// UserB earnSnow and warp 1 week
vm.startPrank(userB);
snow.earnSnow();
assertEq(snow.balanceOf(userB), 1);
vm.warp(block.timestamp + 1 weeks);
vm.stopPrank();
// UserA buySnow.
vm.startPrank(userA);
snow.buySnow{value: FEE}(1);
vm.stopPrank();
// UserB try to earnSnow and fail
// Due to UserA's buySnow, the global timer is reset
vm.startPrank(userB);
vm.expectRevert();
snow.earnSnow();
vm.stopPrank();
}

Recommended Mitigation

  • Use per-user mappings for time-based restrictions

  • Consider adding events for timer resets

  • Add more test cases to verify time-based functionality

mapping(address => uint256) private s_earnTimer;
function buySnow(uint256 amount) external payable canFarmSnow {
// ... minting logic ...
// Remove timer reset
}
function earnSnow() external canFarmSnow {
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
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.