Daily Reset Logic Inconsistency
Description
The contract should use consistent logic for all daily reset mechanisms to ensure synchronization between different daily limits.
The contract uses two different calculation methods for daily resets - division-based calculation for ETH drips and addition-based calculation for token claims, creating potential desynchronization where users can exploit timing differences between the two systems.
@> uint256 currentDay = block.timestamp / 24 hours;
@> if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
@> if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
Risk
Likelihood: Medium
-
The two reset mechanisms will naturally drift apart over time due to different calculation methods
-
Different calculation methods create predictable timing windows where limits are inconsistent
-
Users can observe and exploit the gap between reset timings
Impact: Medium
-
Users may claim more tokens or ETH than intended daily limits by timing claims during desynchronized periods
-
Protocol economics are disrupted through inconsistent limit enforcement
-
Unpredictable faucet behavior confuses legitimate users and reduces trust
Proof of Concept
contract DailyResetExploit {
function demonstrateDesynchronization() external pure returns (
bool ethResetTriggered,
bool tokenResetTriggered,
uint256 timingGap
) {
uint256 deployTime = 1704110200;
uint256 lastDripDay = deployTime / 86400;
uint256 lastFaucetDripDay = deployTime;
uint256 checkTime = 1704067200;
uint256 currentDay = checkTime / 86400;
ethResetTriggered = (currentDay > lastDripDay);
tokenResetTriggered = (checkTime > lastFaucetDripDay + 86400);
uint256 nextMidnight = 1704153600;
uint256 nextTokenReset = deployTime + 86400;
timingGap = nextTokenReset - nextMidnight;
return (ethResetTriggered, tokenResetTriggered, timingGap);
}
function simulateExploitWindow() external pure returns (
uint256 exploitWindowStart,
uint256 exploitWindowEnd,
uint256 windowDuration
) {
uint256 deployTime = 1704110200;
exploitWindowStart = 1704153600;
exploitWindowEnd = 1704196800;
windowDuration = exploitWindowEnd - exploitWindowStart;
return (exploitWindowStart, exploitWindowEnd, windowDuration);
}
}
Attack scenario:
Contract deployed Jan 1 at 12:30 PM
User claims once on Jan 1 (uses up daily allowances)
Jan 2 at 00:01 AM: ETH limit resets but token limit doesn't
User can potentially get ETH again during 12.5 hour window
Jan 2 at 12:31 PM: Token limit finally resets
Pattern repeats daily, creating exploitable windows
Recommended Mitigation
The mitigation standardizes both reset mechanisms to use the same calculation method, ensuring perfect synchronization between ETH and token daily limits. This eliminates exploitable timing windows.
+ // Use single reset tracking for both ETH and tokens
+ uint256 public lastResetDay;
function claimFaucetTokens() public nonReentrant {
// ... all checks first ...
- // Remove inconsistent ETH reset logic
- uint256 currentDay = block.timestamp / 24 hours;
- if (currentDay > lastDripDay) {
- lastDripDay = currentDay;
- dailyDrips = 0;
- }
-
- // Remove inconsistent token reset logic
- if (block.timestamp > lastFaucetDripDay + 1 days) {
- lastFaucetDripDay = block.timestamp;
- dailyClaimCount = 0;
- }
+ // Unified daily reset logic - both limits reset simultaneously
+ uint256 currentDay = block.timestamp / 1 days;
+ if (currentDay > lastResetDay) {
+ lastResetDay = currentDay;
+ dailyDrips = 0; // Reset ETH daily limit
+ dailyClaimCount = 0; // Reset token daily limit
+ emit DailyLimitsReset(currentDay, block.timestamp);
+ }
// ... rest of function logic remains the same ...
}
// Update constructor to initialize unified reset tracking
constructor(
string memory name_,
string memory symbol_,
uint256 faucetDrip_,
uint256 sepEthDrip_,
uint256 dailySepEthCap_
) ERC20(name_, symbol_) Ownable(msg.sender) {
faucetDrip = faucetDrip_;
sepEthAmountToDrip = sepEthDrip_;
dailySepEthCap = dailySepEthCap_;
+ lastResetDay = block.timestamp / 1 days; // Initialize to deployment day
_mint(address(this), INITIAL_SUPPLY);
}
// Add event for transparency
+ event DailyLimitsReset(uint256 indexed dayNumber, uint256 timestamp);
// Remove old state variables that are no longer needed
- uint256 public lastDripDay;
- uint256 public lastFaucetDripDay;