Snowman Merkle Airdrop

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

Timer Contamination Between `Snow::buySnow` and `Snow::eaearnSnow` Functions which pose a pontential Denial-of-Service attack

Root + Impact

Shared State Variable Creates Cross-Function DoS Attack Vector

Description

The contract suffers from a critical design flaw where a single timer variable (`s_earnTimer`) is shared between two distinct functions: `buySnow()` and `earnSnow()`. This creates a dangerous cross-contamination vulnerability where calling one function directly impacts the availability and timing constraints of the other function, leading to potential denial-of-service attacks and complete disruption of the contract's intended functionality.
The vulnerability exists because:
1. Both `buySnow()` and `earnSnow()` update the same `s_earnTimer` variable
2. The `earnSnow()` function checks if `block.timestamp < (s_earnTimer + 1 weeks) `before allowing execution
3. Any call to `buySnow()` resets this timer, potentially blocking `earnSnow()` for another week
4. This creates an unintended dependency between unrelated functions.
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); // state change after external call
}
//audit Timer Contamination Between Functions
@> 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);
//audit Timer Contamination Between Functions
@> s_earnTimer = block.timestamp;
}

Risk

Likelihood:

  1. Zero technical barriers - Anyone with internet can execute this

  2. Extremely low cost - ~$10-50/year to completely break the protocol

  3. High incentive - Competitors, griefers, or anyone wanting to short the token

  4. Accidental triggers - Even legitimate users unknowingly cause DoS by buying tokens

  5. No countermeasures - Contract has zero protection against this attack

Timeline to exploitation:

  • Production deployment: Within 24-48 hours

  • Testnet: Immediately upon discovery

This isn't a "might happen" - it's a "when will it happen" scenario. The vulnerability is too obvious, too cheap, and too devastating for attackers to ignore.

Impact:

CRITICAL SEVERITY - Complete Contract Dysfunction
This vulnerability can render the contract completely worthless through multiple attack vectors:
- An attacker can call `buySnow()` every 6 days and 23 hours
- This resets `s_earnTimer` just before the 1-week cooldown expires
- Result: `earnSnow()` becomes permanently unusable for all users
- Cost: Minimal (just the buy fee for 1 token every ~7 days)
**Griefing Attack Scenarios:**
- Attacker monitors mempool for earnSnow() transactions
- Front-runs with buySnow() calls to reset timer
- Legitimate users are consistently blocked from earning
- Creates frustration and abandonment of the protocol

Proof of Concept

run `forge test --match-test testDoSviaTimerContamination -vvv` with test in `TestSnow.t.sol`
Output:
```ngnix
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/TestSnow.t.sol:TestSnow
[PASS] testDoSviaTimerContamination() (gas: 29571)
Logs:
=== ECONOMIC IMPACT ANALYSIS ===
Attack frequency: Every week
Cost per attack: 5000000000000000000 wei
Annual attack cost: 260000000000000000000 wei
Functions destroyed: earnSnow() (core protocol feature)
Users affected: ALL protocol users
Recovery method: NONE (requires contract redeployment)
COST-BENEFIT ANALYSIS:
- Attacker investment: Minimal ( 260000000000000000000 wei/year)
- Protocol damage: Total (earnSnow() permanently unusable)
- User impact: Complete loss of core functionality
- Token value impact: Severe degradation
function testDoSviaTimerContamination() public {
console2.log("ECONOMIC IMPACT ANALYSIS");
uint256 attacksPerYear = 52; // Once per week
uint256 totalAttackCost = attacksPerYear * FEE;
console2.log("Attack frequency: Every week");
console2.log("Cost per attack:", FEE, "wei");
console2.log("Annual attack cost:", totalAttackCost, "wei");
console2.log("Functions destroyed: earnSnow() (core protocol feature)");
console2.log("Users affected: ALL protocol users");
console2.log("Recovery method: NONE (requires contract redeployment)");
// Show how cheap the attack is relative to damage
console2.log("");
console2.log("COST-BENEFIT ANALYSIS:");
console2.log("- Attacker investment: Minimal (", totalAttackCost, "wei/year)");
console2.log("- Protocol damage: Total (earnSnow() permanently unusable)");
console2.log("- User impact: Complete loss of core functionality");
console2.log("- Token value impact: Severe degradation");
console2.log("");
console2.log("CONCLUSION: Extremely high impact, extremely low cost attack");
console2.log("Makes the contract unsuitable for production deployment");
}

Recommended Mitigation

This vulnerability can single-handedly destroy the protocol's functionality and value, making it unsuitable for production deployment without immediate remediation so i highly recommed Separate Timer Variables
uint256 private s_earnTimer; // Only for earnSnow()
uint256 private s_buyTimer; // Only for buySnow() if needed
function buySnow(uint256 amount) external payable canFarmSnow {
// ... rest of function logic
- s_earnTimer = block.timestamp;
+ s_buyTimer = block.timestamp;
// ... rest of function
}
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; // Only update earn timer
}
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.