Raisebox Faucet

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

Daily ETH Cap Bypass via Incorrect dailyDrips Reset in claimFaucetTokens

Root + Impact

Description

  • Normal behavior:

    • First-time claimers receive Sepolia ETH, bounded by a per-day cap tracked by dailyDrips, which resets once per day.

  • Issue:

    • When the caller is not eligible for ETH (already claimed) or when ETH drips are paused, the code sets dailyDrips to 0. This lets subsequent first-time claimers restart the day’s counter, bypassing the daily cap and allowing excess ETH distribution.

if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 0;
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
);
}
} else {
@> dailyDrips = 0;
}

Risk

  • Likelihood:

    • Occurs whenever a non-first-time claimer calls claimFaucetTokens() after at least one ETH drip has occurred that day.

    • Occurs whenever ETH drips are paused and any claimer calls claimFaucetTokens().

  • Impact:

    • The daily ETH cap (dailySepEthCap) is effectively bypassed; more ETH than intended can be dispensed in a single day.

    • Coordinated or scripted calls can repeatedly reset dailyDrips, accelerating ETH depletion from the contract.

Proof of Concept

// Pseudo-Foundry PoC outline with explanation embedded
contract BypassDailyCap is Test {
RaiseBoxFaucet f;
address firstTimer = makeAddr("firstTimer");
address repeater = makeAddr("repeater"); // already claimed before
function setUp() public {
// daily cap = 0.01 ETH, per-claim drip = 0.005 ETH
f = new RaiseBoxFaucet("RB", "RB", 1000 ether, 0.005 ether, 0.01 ether);
vm.deal(address(f), 1 ether);
vm.warp(3 days);
// Step 1: repeater becomes non-first-timer and consumes 0.005 of the cap
vm.prank(repeater);
f.claimFaucetTokens();
}
function test_Bypass() public {
// Step 2: later in the same "day", repeater calls again;
// this enters the 'else' branch and resets dailyDrips to 0.
vm.prank(repeater);
f.claimFaucetTokens(); // dailyDrips = 0 (bug)
// Step 3: a fresh first-timer claims and still receives 0.005 ETH,
// even though the cap should have blocked further drips.
vm.prank(firstTimer);
f.claimFaucetTokens();
// Outcome: 0.01 ETH dripped within the same day despite the cap, due to the reset.
}
}
  • Explanation:

    • The second call by repeater executes the else branch and resets dailyDrips to 0, undoing prior accounting. This makes the system treat the day as if no ETH was dripped, allowing another first-timer to receive ETH beyond the intended daily cap.

Recommended Mitigation

diff --git a/src/RaiseBoxFaucet.sol b/src/RaiseBoxFaucet.sol
@@
- } else {
- dailyDrips = 0;
- }
+ }
  • Rationale: dailyDrips denotes cumulative ETH dripped for the current day and should only reset when the day changes (already handled via currentDay > lastDripDay). Resetting it for non-first-time claimers or during paused states breaks the cap invariant.

  • Optional hardening:

    • Add an invariant test to assert dailyDrips <= dailySepEthCap across all call paths within a day.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months 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.

Give us feedback!