Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: medium
Valid

[H-3] Sliding Window Reset — Coordinated Claim Timing Can Bypass Intended Fairness

[H-3] Sliding Window Reset — Coordinated Claim Timing Can Bypass Intended Fairness

Description

  • Normal behavior: The faucet should reset its per-day counters on a predictable daily boundary (for example, a day-index such as UTC calendar day or a consistent 24-hour bucket). Each reset must be deterministic and not controllable by a caller, so limits like dailyDrips and dailyClaimCount enforce the intended per-day caps fairly for all users.

  • Specific issue: the contract records the reset time using an absolute timestamp (e.g. lastFaucetDripDay = block.timestamp) and/or mixes timestamp-based resets with day-index-based checks. This turns the “day” into a sliding 24‑hour window that an on-chain actor can intentionally trigger at an advantageous time, thereby moving the next reset to a chosen timestamp + 24h and enabling coordinated abuse around that new window.

// Root cause (excerpted / annotated from the codebase)
// @> marks highlight the problematic lines that cause a sliding reset window
uint256 currentDay = block.timestamp / 24 hours;
// ... some code that decides to reset dailyDrips ...
if (block.timestamp >= lastFaucetDripDay + 1 days) {
// reset counter for the day
dailyDrips = 0;
// @> sliding-window root cause: store the timestamp of reset (not a day index)
lastFaucetDripDay = block.timestamp; // @> this makes the next reset exactly 24h after the moment
}
// Elsewhere (inconsistent use):
// dailyDrips uses currentDay = block.timestamp / 24 hours
// dailyClaimCount resets using `timestamp + 1 days` (mixing day definitions)

Risk

Likelihood:High

  • An on-chain actor triggers a faucet drip (or otherwise causes the reset path to execute) at time T deliberately, making the next reset occur at T + 24 hours — this will happen whenever a transaction executes the reset-branch around the attacker’s chosen time.

  • Coordinated actors (multiple addresses or timed scripts) submit many claims concentrated around the sliding-window start times that they control, maximizing the amount claimable during each engineered 24‑hour window. This pattern is easy to schedule on-chain or via bots.

Impact:High

  • The per-day throttling mechanism becomes manipulable: attackers can concentrate claims to capture a disproportionate share of the daily cap, violating fairness and intended limits.

  • In combination with other vulnerabilities (for example, reentrancy or missing per-user checks), this can materially increase funds/tokens an attacker can obtain in practice. Even by itself it enables predictable exploitation and undermines guarantee of “one fair reset per calendar day.”

Proof of Concept

lastFaucetDripDay is stored in contract storage and, in this codebase, declared public. That means anyone can directly read the current value via the generated getter (contract.lastFaucetDripDay()) or by reading the contract storage slot with RPC (eth_getStorageAt). Because the value is public and easily accessible, attackers and bots can precisely schedule reset-triggering calls and coordinated claim bursts.

Quick examples:

// ethers.js - call the public getter
const abi = ["function lastFaucetDripDay() view returns (uint256)"];
const c = new ethers.Contract(addr, abi, provider);
const last = await c.lastFaucetDripDay();
console.log(last.toString());
// Minimal attack sequence (conceptual):
// 1) Attacker calls the faucet to trigger the reset branch at time T.
// 2) Contract sets lastFaucetDripDay = T (timestamp).
// 3) Attacker waits ~24 hours (T + 24h) and issues many claims (or coordinates many addresses)
// around that T + 24h boundary to maximize per-window collection.
contract POC {
RaiseBoxFaucet faucet;
constructor(address _f) { faucet = RaiseBoxFaucet(_f); }
// Step 1: trigger the reset at attacker-chosen time
function triggerReset() external {
// call the faucet in a way that reaches the reset branch
faucet.claimFaucetTokens();
}
// Step 2: after ~24h, run many claims (or call via multiple addresses)
function massClaim(address[] calldata addrs) external {
for (uint i = 0; i < addrs.length; i++) {
// either perform calls from different accounts or instruct off-chain bots to call around this time
// this demonstrates the coordination/abuse vector; exact calling pattern depends on on-chain roles
}
}
}

Recommended Mitigation

- // store the exact timestamp when the last reset occurred
- uint256 public lastFaucetDripDay;
+ // store a day index (e.g. days since unix epoch). Use the same day-index everywhere.
+ uint256 public lastFaucetDripDayIndex;
- uint256 currentDay = block.timestamp / 24 hours;
- if (block.timestamp >= lastFaucetDripDay + 1 days) {
- dailyDrips = 0;
- lastFaucetDripDay = block.timestamp;
- }
+ uint256 currentDay = block.timestamp / 1 days; // explicit day index
+ if (currentDay > lastFaucetDripDayIndex) {
+ // reset per-day counters for new calendar/day bucket
+ dailyDrips = 0;
+ lastFaucetDripDayIndex = currentDay;
+ }
// Also: ensure any other per-day counters use the same day index logic,
// e.g. dailyClaimCount must reset when currentDay > lastClaimDayIndex (not via timestamp+1 days).
// Also: use the same `currentDay` logic everywhere (dailyDrips, dailyClaimCount, etc.)
// so "day" is a single, consistent unit across the contract.
Updates

Lead Judging Commences

inallhonesty Lead Judge 6 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Inconsistent day calculation methods cause desynchronization between ETH and token daily resets.

Support

FAQs

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