Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: high
Likelihood: high

Daily ETH Cap Bypass via Incorrect `dailyDrips` Reset

Author Revealed upon completion

Description

The claimFaucetTokens() function tracks the amount of Sepolia ETH dripped per day using dailyDrips and enforces a per‑day cap dailySepEthCap for first‑time claimers.

However, when a claimer is not eligible for ETH (e.g., not a first‑timer or drips are paused), the code resets dailyDrips to 0 in the else branch. This allows any non‑first‑time claimer to zero the daily accounting, enabling additional first‑time claimers to receive ETH the same day and effectively bypassing the daily ETH cap.

if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... may increase dailyDrips when dripping ETH ...
} else {
dailyDrips = 0; // BUG: Resets the day's running total even though no drip occurred
}

This logic should not reset the daily accounting when no ETH is dripped. dailyDrips should only reset when the day changes.

Risk

Likelihood: High

  • Any non‑first‑time claimer can perform a regular claim at any time during the day to reset dailyDrips to 0

  • No special timing is required beyond being able to call claimFaucetTokens()

  • Deterministic and repeatable within the same day

Impact: High

  • Bypasses the daily ETH distribution cap, causing the contract to distribute more ETH than intended

  • Enables repeated ETH drains within the same day by alternating between non‑first‑time claimers (to reset) and new first‑time claimers (to receive ETH)

  • Breaks fairness and budget assumptions of the faucet

Proof of Concept

The following test demonstrates the bypass with a daily cap equal to exactly one drip. After one first‑time claimer consumes the daily cap, a non‑first‑time claimer resets dailyDrips to 0, allowing another first‑time claimer to receive ETH again on the same day.

function testDailyEthCapBypassDueToResetBug() public {
// Setup: cap = one first-time drip per day
RaiseBoxFaucet f = new RaiseBoxFaucet(
"raiseboxtoken",
"RB",
1000 * 10 ** 18,
0.005 ether, // sepEthAmountToDrip per first-timer
0.005 ether // daily cap allows only 1 first-time drip per day
);
vm.deal(address(f), 1 ether);
// Day 0: prepare a non-first-time claimer (user12)
vm.warp(3 days);
vm.prank(user12);
f.claimFaucetTokens();
assertTrue(f.getHasClaimedEth(user12));
// Day 1: cooldown satisfied
vm.warp(6 days);
// Fill today's cap with a single first-time claimer
vm.prank(user1);
f.claimFaucetTokens();
assertEq(address(user1).balance, 0.005 ether);
assertEq(f.dailyDrips(), 0.005 ether);
// Another first-timer should NOT receive ETH due to cap reached
vm.prank(user2);
f.claimFaucetTokens();
assertEq(address(user2).balance, 0);
assertEq(f.dailyDrips(), 0.005 ether);
// Non-first-time claimer (user12) claims; BUG resets dailyDrips to 0 in else branch
vm.prank(user12);
f.claimFaucetTokens();
assertEq(f.dailyDrips(), 0);
// After reset, a new first-timer improperly receives ETH again the same day
vm.prank(user4);
f.claimFaucetTokens();
assertEq(address(user4).balance, 0.005 ether);
}

To run just this test:

forge test --match-test testDailyEthCapBypassDueToResetBug -vv

Recommended Mitigation

Remove the erroneous reset and reset dailyDrips only when the day changes.

function claimFaucetTokens() public {
// ... checks ...
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;
// ... send ETH and emit event ...
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip
? "Faucet out of ETH"
: "Daily ETH cap reached"
);
}
- } else {
- dailyDrips = 0; // ❌ Remove: incorrectly resets daily tracking
- }
+ } // no reset here
// ... rest of function ...
}

Support

FAQs

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