Snowman Merkle Airdrop

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

earnSnow uses one shared timer for all users, so any claim or purchase, even buySnow(0), blocks everyone else from earning their weekly token

Root + Impact

earnSnow uses one shared timer for all users, so any claim or purchase, even buySnow(0), blocks everyone else from earning their weekly token.

Description

The earnSnow function is supposed to give each user one token per week.

The problem is that instead of a separate timer for each user, there is one timer for everyone.

Anyone who claims their weekly token, or even just calls buySnow(0), resets the timer for all other users.

This prevents them from receiving their weekly token.

@> uint256 private s_earnTimer;
​
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);
}
​
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;
}

Risk

Likelihood: High. Any user can trigger it. Calling buySnow(0) costs nothing except gas, because msg.value (0) equals s_buyFee * 0.

Impact: Medium. The free weekly Snow distribution, a core feature of the protocol, can be blocked for all users. An attacker only needs to call buySnow(0) once a week to keep it blocked permanently.

Proof of Concept

The following tests show that one user's action blocks other users from calling earnSnow.

function testBuySnowZeroBlocksEarnSnowForEveryone() public {
address attacker = makeAddr("attacker");
address victim = makeAddr("victim");
​
// attacker buys 0 tokens for free, resetting the shared timer
vm.prank(attacker);
snow.buySnow(0);
​
// victim cannot earn their weekly token
vm.prank(victim);
vm.expectRevert(Snow.S__Timer.selector);
snow.earnSnow();
}
​
function testOneUserEarningBlocksOthers() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
​
vm.prank(alice);
snow.earnSnow();
​
// bob has never earned, but is still blocked
vm.prank(bob);
vm.expectRevert(Snow.S__Timer.selector);
snow.earnSnow();
}

Recommended Mitigation

Use a separate timer for each user, and do not reset it in buySnow.

- uint256 private s_earnTimer;
+ mapping(address => uint256) private s_earnTimer;
​
function buySnow(uint256 amount) external payable canFarmSnow {
+ if (amount == 0) revert S__ZeroValue();
...
- s_earnTimer = block.timestamp;
emit SnowBought(msg.sender, amount);
}
​
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;
}
​
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour 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!