Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Only one user can earn tokens weekly due to missing per-user logic in `Snow::earnSnow()`

Summary

The Snow::earnSnow() function fails to track weekly token claims on a per-user basis. As a result, only one user can claim a token per week globally, instead of each user being allowed to claim once per week individually which breaks the core functionality.

Description

The Snow::earnSnow() function currently uses a single global timestamp s_earnTimer to track when a claim was last made. This implementation restricts the entire system to one token claim per week, regardless of how many users are trying to claim.

This violates a core design invariant which states "The Snow token can either be earned for free onece a week, or bought at anytime, up until during the ::FARMING_DURATION is over."

Due to this flaw, after 12 weeks, only 12 tokens can ever be earned globally, completely blocking all other users from participating. This creates a Denial-of-Service once one user has claimed and severely limits the intended distribution of tokens.

The correct number of distributed tokens is supposed to be 12 tokens * n where "n" is the number of participants.

Vulnerable Snow::earnSnow

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;
}

Impact

The severity of this issue is High for the following reasons:

  • Impact: High: a core invariant in the protocol is broken and functionality is limited

  • Likelihood: High - this issue is trigger each week whenever earnSnow is called.

The existence of this issue causes:

  1. Decreased distribution of Snow tokens than the Protocol intended. This will affect its acceptance and use within the blockchain system.

  2. User dissatisfaction as they cannot claim free tokens and therefore are forced to make purely financial investments.

Proof of concept

As proof of the validity of the issue, I have provided the below runnable code.

Description

  1. First User - ashley - calls Snow:;earnSnow and is minted a token

  2. The second user - victory - within the same week tries to obtain a free Snow token.

  3. Victory calls Snow::earnSnow but this call reverts due to the contract tracking claims globally instead of per-user.

Code

Run with: forge test --mt testOnlyOneUserCanEarnFreeSnow -vvv

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "src/Snow.sol";
import {DeploySnow} from "script/DeploySnow.s.sol";
import {MockWETH} from "src/mock/MockWETH.sol";
contract TestSnow is Test {
Snow snow;
DeploySnow deployer;
MockWETH weth;
address collector;
uint256 FEE;
address jerry;
address victory;
address ashley;
function setUp() public {
deployer = new DeploySnow();
snow = deployer.run();
weth = deployer.weth();
collector = deployer.collector();
FEE = deployer.FEE();
jerry = makeAddr("jerry");
victory = makeAddr("victory");
ashley = makeAddr("ashley");
weth.mint(jerry, FEE);
deal(victory, 100 ether);
}
function testOnlyOneUserCanEarnFreeSnow() public {
//First User has claimed
vm.prank(ashley);
snow.earnSnow();
//Another User Cannot claim free Token
vm.prank(victory);
vm.expectRevert(); // Will revert as there is no tracking of msg.sender
snow.earnSnow();
assert(snow.balanceOf(ashley) == 1);
assert(snow.balanceOf(victory) == 0);
console2.log("Final Balance of Ashely in 1 week: ", snow.balanceOf(ashley));
console2.log("Balance of Victory in 1 week: ", snow.balanceOf(victory));
}
}

Mitigation

The recommended mitigation is to replace s_earnTimer with a mapping that contains a user-specific timestamp each time they call earnSnow. Each call will check the mapping to verify that the user is calling a week after they last called.

// Mapping declaration
mapping(address user => uint256 timeClaimed) private hasUserClaimed;
//Implementation in function
function earnSnow() external canFarmSnow {
uint256 timeClaimed = hasUserClaimed[msg.sender];
if (block.timestamp < timeClaimed + 1 weeks) { //@dev: checking the last time the user claimed
revert S__Timer();
}
_mint(msg.sender, 1);
hasUserClaimed[msg.sender] = block.timestamp;
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
5 months ago
yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

blackgrease Submitter
5 months ago
yeahchibyke Lead Judge
5 months ago
yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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