buySnow(0) Resets the Global earnSnow Timer, Permanently DoS-ing the Weekly RewardSnow is an ERC-20 that grants one token per week through the public earnSnow function. A single global state variable, s_earnTimer, enforces the one-week cooldown: earnSnow succeeds only when block.timestamp >= s_earnTimer + 1 weeks. Users can also purchase tokens through buySnow with ETH or WETH until the 12-week farming window ends.
buySnow accepts amount = 0 and unconditionally executes s_earnTimer = block.timestamp. An attacker can call buySnow(0) with zero value (because msg.value == s_buyFee * 0 == 0, taking the _mint(0) branch) and reset the shared cooldown timer for free. Every earnSnow call then reverts with S__Timer() for the following week; repeating the call once per week locks out the reward for the entire farming period.
Likelihood: High
The attack requires only a call to buySnow(0) with no ETH and no token approval — it succeeds the moment it is submitted after a cooldown expiry.
It is repeatable once every 7 days, sustaining the denial of service across the entire 12-week farming window at only gas cost.
Impact: Low–Medium
earnSnow becomes permanently unavailable, denying every user the weekly token reward (Denial of Service).
The farming incentive is broken at zero economic cost to the attacker, undermining the token distribution design.
No funds are stolen; the loss is functional availability of the reward mechanism.
Severity: Low–Medium (High likelihood × Low–Medium impact).
The PoC establishes the causal link between buySnow(0) and the DoS:
A victim earns the first token (global timer is now active).
After vm.warp(+1 week), earnSnow succeeds again — this baseline proves the function is usable at that moment.
After the cooldown expires a second time, the attacker calls buySnow(0) with zero value, which takes the _mint(0) branch and resets s_earnTimer to the current block.
earnSnow now reverts with S__Timer() where it would otherwise have succeeded, confirming the reset caused the lockout.
The core attack path uses zero vm.* cheatcodes; vm.warp is used only to advance the one-week cooldown for testing (2 uses, with the reason noted in the header, within the ≤3 budget).
Verification: the PoC was executed in the local WSL2 network-isolated sandbox with double replay — execution_status = succeeded, verdict = supports (artifact 6e0d39e1-bf9a-55de-a346-afdce2e8fdef, toolchain 7694c3aa…), so the denial of service is confirmed to be reproducible.
Reject zero-amount purchases so a caller can never reset the shared cooldown for free. Optionally, move the timer update so it only happens on a genuine purchase, and consider tracking the cooldown per user instead of globally so one caller cannot affect all others.
The zero-amount check alone fully neutralizes the free reset described above, since msg.value == s_buyFee * 0 == 0 is no longer reachable; any subsequent purchase with amount > 0 pays the fee and legitimately updates the timer.
## Description: The `Snow::buySnow` function contains a critical flaw where it resets a global timer `(s_earnTimer)` to the current block timestamp on every invocation. This timer controls eligibility for free token claims via `Snow::earnSnow()`, which requires 1 week to pass since the last timer reset. As a result: Any token purchase `(via buySnow)` blocks all free claims for all users for 7 days Malicious actors can permanently suppress free claims with micro-transactions Contradicts protocol documentation promising **"free weekly claims per user"** ## Impact: * **Complete Denial-of-Service:** Free claim mechanism becomes unusable * **Broken Protocol Incentives:** Undermines core user acquisition strategy * **Economic Damage:** Eliminates promised free distribution channel * **Reputation Harm:** Users perceive protocol as dishonest ```solidity 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); } ``` ## Risk **Likelihood**: • Triggered by normal protocol usage (any purchase) • Requires only one transaction every 7 days to maintain blockage • Incentivized attack (low-cost disruption) **Impact**: • Permanent suppression of core protocol feature • Loss of user trust and adoption • Violates documented tokenomics ## Proof of Concept **Attack Scenario:** Permanent Free Claim Suppression * Attacker calls **buySnow(1)** with minimum payment * **s\_earnTimer** sets to current timestamp (T0) * All **earnSnow()** calls revert for **next 7 days** * On day 6, attacker repeats **buySnow(1)** * New timer reset (T1 = T0+6 days) * Free claims blocked until **T1+7 days (total 13 days)** * Repeat step **4 every 6 days → permanent blockage** **Test Case:** ```solidity // Day 0: Deploy contract snow = new Snow(...); // s_earnTimer = 0 // UserA claims successfully snow.earnSnow(); // Success (first claim always allowed) // Day 1: UserB buys 1 token snow.buySnow(1); // Resets global timer to day 1 // Day 2: UserA attempts claim snow.earnSnow(); // Reverts! Requires day 1+7 = day 8 // Day 7: UserC buys 1 token (day 7 < day 1+7) snow.buySnow(1); // Resets timer to day 7 // Day 8: UserA retries snow.earnSnow(); // Still reverts! Now requires day 7+7 = day 14 ``` ## Recommended Mitigation **Step 1:** Remove Global Timer Reset from `buySnow` ```diff function buySnow(uint256 amount) external payable canFarmSnow { // ... existing payment logic ... - s_earnTimer = block.timestamp; emit SnowBought(msg.sender, amount); } ``` **Step 2:** Implement Per-User Timer in `earnSnow` ```solidity // Add new state variable mapping(address => uint256) private s_lastClaimTime; function earnSnow() external canFarmSnow { // Check per-user timer instead of global if (s_lastClaimTime[msg.sender] != 0 && block.timestamp < s_lastClaimTime[msg.sender] + 1 weeks ) { revert S__Timer(); } _mint(msg.sender, 1); s_lastClaimTime[msg.sender] = block.timestamp; // Update user-specific timer emit SnowEarned(msg.sender, 1); // Add missing event } ``` **Step 3:** Initialize First Claim (Constructor) ```solidity constructor(...) { // Initialize with current timestamp to prevent immediate claims s_lastClaimTime[address(0)] = block.timestamp; } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.