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");
vm.prank(attacker);
snow.buySnow(0);
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();
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;
}