Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: medium
Likelihood: high

`earnSnow()` uses a global timer instead of per-user tracking, allowing Sybil-based inflation of Snow token supply

Author Revealed upon completion

Root + Impact

Description

Normal behavior:
The earnSnow() function is designed to allow users to claim 1 free SNOW token every 7 days as part of a weekly earning mechanism during the farming period.

Issue:
The contract uses a global timestamp (s_earnTimer) to control the cooldown for all users, instead of tracking cooldowns per individual address. As a result, anyone can earn tokens using new wallets with no restriction, allowing attackers to bypass the 1-per-week rule and claim repeatedly from many EOAs.

function earnSnow() external canFarmSnow {
if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer(); // @> Shared global timer throttles all users together
}
_mint(msg.sender, 1); // @> Allows any new wallet to claim once per global cooldown period
s_earnTimer = block.timestamp; // @> Overwrites global timer, not specific to user
}

Risk

Likelihood: High

  • This occurs whenever a user generates a new wallet address and calls earnSnow(), since the contract does not track earning history on a per-user basis.

  • Users can automate the creation of fresh EOAs using scripts or bots to repeatedly claim Snow tokens with no penalty or cooldown enforcement, making this attack both practical and scalable.


Impact: Medium

  • Users can farm an unlimited number of Snow tokens by repeatedly calling earnSnow() from new wallets, completely bypassing the intended "1 token per week" restriction.

  • The SNOW token supply becomes inflated beyond the design parameters, damaging the tokenomics and fairness of the protocol. This may also reduce the value of Snowman NFTs tied to SNOW ownership and weaken overall trust in the system.

Proof of Concept

The PoC demonstrates that an attacker can bypass the weekly earning restriction by calling earnSnow() from multiple freshly generated wallets. Since the contract uses a shared global cooldown timer, each wallet is treated as a new claimant, allowing the attacker to mint unlimited SNOW tokens in a single farming window.

// Attacker automates multiple wallet claims using fresh EOAs
for (uint256 i = 0; i < 100; i++) {
// Simulate generating a new wallet
address newWallet = createNewWallet(); // pseudo-code
// Fund the wallet with just enough ETH to call earnSnow
fundWallet(newWallet, 0.01 ether);
// From each new wallet, call earnSnow
vm.prank(newWallet); // Using Foundry-style cheatcode for impersonation
snowContract.earnSnow(); // Each wallet bypasses global cooldown
}

Recommended Mitigation

The contract should track the weekly earning cooldown per user address, not globally.
By using a mapping(address => uint256) to store individual user timestamps, each address can earn SNOW once every 7 days, as originally intended.

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

Support

FAQs

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