Root + Impact
Description
The contract uses two separate day tracking mechanisms for different claim types: lastDripDay for ETH refills and lastFaucetDripDay for token claims. These independent tracking systems can reset at different times and create state inconsistencies.
The specific issue is that having two separate day counters for the same user creates confusion and potential for state desynchronization, as they track the same concept (last claim day) but for different purposes.
mapping(address => uint256) public lastDripDay;
mapping(address => uint256) public lastFaucetDripDay;
function refillSepEth() external {
uint256 currentDay = block.timestamp / 1 days;
require(
currentDay > lastDripDay[msg.sender],
"Already claimed today"
);
lastDripDay[msg.sender] = currentDay;
}
function claimFaucetTokens() external {
uint256 currentDay = block.timestamp / 1 days;
require(
currentDay > lastFaucetDripDay[msg.sender] + 2,
"Cooldown period not met"
);
lastFaucetDripDay[msg.sender] = currentDay;
}
Risk
Likelihood:
-
Occurs during normal contract operation
-
Affects all users who use both functions
-
No special conditions required
-
State inconsistency is guaranteed
Impact:
-
Confusing state management
-
Potential for logic errors in future updates
-
Increased gas costs (two storage slots per user)
-
Difficult to reason about user claim history
Proof of Concept
This test demonstrates the state inconsistency:
Setup: We deploy the contract and have a user interact with both functions
Observation: Two separate day counters track similar information
Result: State confusion and potential for errors
The issue occurs because:
-
Two mappings track "last claim day" concept
-
They update independently
-
No synchronization between them
-
Creates unnecessary complexity
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
import {RaiseBoxToken} from "../src/RaiseBoxToken.sol";
contract InconsistentDayTrackingTest is Test {
RaiseBoxFaucet faucet;
RaiseBoxToken token;
address owner = makeAddr("owner");
address user = makeAddr("user");
function setUp() public {
vm.startPrank(owner);
token = new RaiseBoxToken();
faucet = new RaiseBoxFaucet(address(token));
token.mintFaucetTokens(address(faucet), 1_000_000 * 10**18);
vm.deal(address(faucet), 100 ether);
vm.stopPrank();
}
function testInconsistentDayTracking() public {
vm.prank(user);
faucet.claimFaucetTokens();
uint256 lastFaucetDripDay = faucet.lastFaucetDripDay(user);
uint256 lastDripDay = faucet.lastDripDay(user);
assertTrue(lastFaucetDripDay > 0);
assertEq(lastDripDay, 0);
vm.warp(block.timestamp + 1 days);
vm.prank(user);
faucet.refillSepEth();
lastFaucetDripDay = faucet.lastFaucetDripDay(user);
lastDripDay = faucet.lastDripDay(user);
assertTrue(lastFaucetDripDay > 0);
assertTrue(lastDripDay > 0);
assertEq(lastDripDay, lastFaucetDripDay + 1);
}
function testDayTrackingDesynchronization() public {
uint256 currentDay = block.timestamp / 1 days;
vm.prank(user);
faucet.claimFaucetTokens();
assertEq(faucet.lastFaucetDripDay(user), currentDay);
assertEq(faucet.lastDripDay(user), 0);
}
}
Recommended Mitigation
Consolidate into a single day tracking mechanism:
- mapping(address => uint256) public lastDripDay;
- mapping(address => uint256) public lastFaucetDripDay;
+ mapping(address => uint256) public lastClaimDay;
+ mapping(address => uint256) public lastETHRefillDay;
function refillSepEth() external {
uint256 currentDay = block.timestamp / 1 days;
require(
- currentDay > lastDripDay[msg.sender],
+ currentDay > lastETHRefillDay[msg.sender],
"Already claimed today"
);
- lastDripDay[msg.sender] = currentDay;
+ lastETHRefillDay[msg.sender] = currentDay;
}
function claimFaucetTokens() external {
uint256 currentDay = block.timestamp / 1 days;
require(
- currentDay > lastFaucetDripDay[msg.sender] + 2,
+ currentDay > lastClaimDay[msg.sender] + 2,
"Cooldown period not met"
);
- lastFaucetDripDay[msg.sender] = currentDay;
+ lastClaimDay[msg.sender] = currentDay;
}