Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Users Cannot Earn Free Snow Without First Buying Due to Global Timer Initialization

Description

The protocol documentation and intended design state that users can acquire Snow tokens in two ways:

  1. By buying them (paying ETH or WETH).

  2. By earning 1 free token per week during the 12-week farming period — independently of buying.

However, the current implementation uses a single global variable s_earnTimer to track the last earn/buy timestamp, which is initialized as 0, considering that it will be replaced with the per user track using a mapping. If the user don't buy one or more token than he can also not earn the token for free every week as intended

solidity

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;
}

The critical issue is the s_earnTimer != 0 check:

  • At deployment, s_earnTimer is initialized to 0.

  • It only becomes non-zero when any user calls buySnow() (or earnSnow() after it's been set).

  • Until the first buy occurs globally, every call to earnSnow() reverts because s_earnTimer == 0.

This creates a permanent lockout for the free earning feature until someone buys tokens.

Risk

  • Contradicts documented design: Users expecting to earn free Snow weekly are completely blocked if no one has ever bought.

  • Chicken-and-egg problem: New users cannot earn for free → low incentive to participate → fewer buyers → feature remains locked.

  • Centralization of activation: The free earning mechanic is effectively gated behind a single global action (first buy).

  • Unfair distribution: Early adopters who buy unlock the earn feature for everyone, including those who never paid.

  • Potential permanent disable: If no one ever buys (e.g., due to high fee or lack of interest), the weekly earn is permanently unavailable.

This significantly undermines the fairness and accessibility of the farming mechanism.

Proof of Concept (Foundry Test)

solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
contract SnowEarnRequiresBuyTest is Test {
Snow snow;
address weth = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
uint256 constant BUY_FEE = 1; // Matches constructor parameter
address collector = makeAddr("collector");
address user = makeAddr("user");
function setUp() public {
snow = new Snow(weth, BUY_FEE, collector);
vm.deal(user, 10 ether);
}
function test_CannotEarnSnowWithoutEverBuying() public {
// Advance time far into the future (>100 weeks)
vm.warp(block.timestamp + 100 weeks + 1 days);
console2.log("User Snow balance before:", snow.balanceOf(user)); // 0
// User attempts to earn free Snow without anyone ever buying
vm.prank(user);
vm.expectRevert(); // Reverts due to s_earnTimer == 0
snow.earnSnow();
assertEq(snow.balanceOf(user), 0, "User still has zero tokens");
}
}

Test Result: The test passes, confirming that even after more than 100 weeks, a user who has never bought (and no one else has) is unable to call earnSnow() successfully. The transaction reverts due to the s_earnTimer == 0 condition.

Recommended mitigation

Replace the global s_earnTimer with a per-user mapping to allow independent weekly earning, and remove the non zero check in the earnsnow function:

solidity

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

Optional: If desired, buySnow() can also update the user's s_lastEarnTime to reset their weekly cooldown.

This change ensures:

  • Every user can earn 1 Snow per week starting immediately after deployment.

  • No dependency on global buy activity.

  • Full alignment with intended "buy or earn weekly" design.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!