Snowman Merkle Airdrop

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

User cant Earn Snow token in `earnSnow` function because of false time implementation in `buySnow`

Description

  • In the snow.sol contract, users are eligible to earn Snow tokens after a predetermined waiting period of one week, as specified in the contract's logic

impact

The function buySnow contains a vulnerability related to timestamp tracking. Specifically, the contract stores block.timestamp in a variable that gets overwritten whenever a new user buys Snow tokens. This leads to the following issues :

  • A previous user's progress toward claiming their tokens is lost, as the timestamp gets reset by the latest transaction.

  • If a user attempts to claim their tokens after another user has triggered earnSnow, they will be unable to do so, since the stored timestamp no longer reflects their original eligibility period.

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);
}
//@audit - it should be mapping
@> 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 - variable is used
@> s_earnTimer = block.timestamp;
}

Risk

Likelihood:

  • This issue occurs whenever a new user triggers buySnow, as the stored timestamp is overwritten.

  • Users attempting to claim their Snow tokens after another user has bought snow token will lose their progress, making it a recurring problem in active environments.

Impact:

  • Loss of expected rewards: Users eligible to claim tokens may find their progress wiped, creating frustration and reducing trust in the system.

Proof of Concept

This test demonstrates how buySnow unintentionally resets user eligibility when a new user claims Snow tokens.

  1. Ashley earns Snow tokens successfully.

  2. After a week, Jerry interacts with the contract, triggering an update to the global timestamp variable.

  3. Ashley’s claim fails, as her progress is overwritten, preventing her from earning additional tokens.

  4. For this purpose , a getter function is created to get the timestamp and prove that its updated when ever a new user claim there Earned Snow Token.

Issue

Since the contract tracks a single timestamp instead of storing values per user, any new interaction resets prior eligibility, leading to lost rewards.

//created a getter function to see the timestamp
+ function getEarnedTime() external view returns(uint256){
+ return s_earnTimer;
+ }
function testCanEarnSnow() public {
//1st user earn snow
vm.prank(ashley);
snow.earnSnow();
assert(snow.balanceOf(ashley) == 1);
//1 week passed
vm.warp(block.timestamp + 1 weeks);
// new user came after 1 week
weth.mint(jerry, FEE);
vm.startPrank(jerry);
weth.approve(address(snow), FEE);
snow.buySnow(1);
vm.stopPrank();
assert(weth.balanceOf(address(snow)) == FEE);
assert(snow.balanceOf(jerry) == 1);
//getter function created by auditor
assertEq(block.timestamp, snow.getEarnedTime());
//ashly claim fail for 2nd snow token coz time variable is updated
vm.startPrank(ashley);
vm.expectRevert(Snow.S__Timer.selector);
snow.earnSnow();
vm.stopPrank();
}

Recommended Mitigation

The issue arises due to a single global timestamp (s_earnTimer) being used for tracking Snow token earnings. This leads to progress resets whenever a new user interacts with the contract, causing previous users to lose their eligibility.


Changes in Contract Structure

  • Previous approach: uint256 private s_earnTimer;

  • Updated approach: mapping(address => uint) private s_earnTimer;

This ensures each user has an independent timestamp, preventing unintended overwrites.


//add this line in place of the variable at the top of the contract
- uint256 private s_earnTimer;
+ mapping(address => uint) private s_earnTimer;
function earnSnow() external canFarmSnow {
- if (s_earnTimer != 0 && block.timestamp < (s_earnTimer + 1 weeks)) {
revert S__Timer();
}
+ 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;
}
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);
}
//@audit - it should be a mapping
- s_earnTimer = block.timestamp;
+ s_earnTimer[msg.sender] = block.timestamp;
emit SnowBought(msg.sender, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 5 days 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.