A shared public timer (s_earnTimer) in the buySnow function is updated, allowing any user to reset the weekly waiting period for all users, thus preventing everyone from calling earnSnow.
Normal Behavior:
The earnSnow function is designed to allow users to earn a reward (one Snow token) once a week. The contract enforces this condition by checking the public timestamp s_earnTimer: if it is non-zero and less than a week has passed since its last update, the call is canceled. After a week, earnSnow can be called again, and s_earnTimer is updated to the current time.s
The problem of code:
The buySnow function also updates s_earnTimer to block.timestamp whenever a user buys Snow tokens. This means that if a malicious user buys even a small amount of Snow (such as a single token), the global timer resets to the current time, forcing a new one-week waiting period on every user. Consequently, no one will be able to call earnSnow for the next seven days, effectively breaking the rewards mechanism.
Likelihood:
High – The attack can be performed by any user, even with a negligible amount of Snow purchased.
The condition for the DoS is automatically met every time someone calls buySnow; no special permissions or preconditions are required.
Once the attack is executed, the DoS lasts for a full week, after which the attacker can repeat the process immediately.
Impact:
Loss of functionality – Users cannot earn Snow tokens for extended periods, breaking the core incentive mechanism of the contract.
Reputational damage – Users expecting weekly rewards will be frustrated, potentially leading to loss of trust and abandonment of the protocol.
Economic harm – If Snow tokens have value, preventing distribution can affect the project’s tokenomics and user participation.
Attack steps:
A legitimate user (victim) calls earnSnow() for the first time – succeeds because the global timer s_earnTimer is zero.
The attacker calls buySnow with amount = 1, paying the required fee (1 ETH in this example). Inside buySnow, after minting the tokens, the contract updates s_earnTimer = block.timestamp.
Now any user, including the victim, trying to call earnSnow will have their transaction reverted with S__Timer() because less than a week has passed since the attacker’s purchase.
The victim must wait a full week before being able to earn again.
As soon as the week passes and the victim earns once more, the attacker can repeat step 2, resetting the timer for another week.
The attack is asymmetric: the attacker spends a minimal amount (1 token’s worth) to deny the reward mechanism to every other user. In the provided PoC, the cost is 1 ETH, but the fee could be lower depending on the contract’s s_buyFee. The attack can be automated and repeated indefinitely, effectively rendering the earnSnow feature unusable.
PoC code (Foundry test):
The test below simulates the attack with two actors: attacker and victim. It verifies that after each attacker purchase, the victim’s earnSnow call fails until a week passes, and then the pattern repeats
The core issue is the global cooldown mechanism. To fix it, we must replace the single s_earnTimer with a per‑user mapping that tracks the last time each individual called earnSnow. This ensures that purchases by others cannot interfere with a user’s ability to earn.
## 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.