Root + Impact
Description
The dailySepEthCap mechanism designed to limit daily Sepolia ETH distribution can be completely bypassed, allowing the faucet to distribute significantly more ETH than intended. This occurs because the dailyDrips counter is incorrectly reset to 0 whenever a repeat claimer (user who has already claimed ETH before) calls claimFaucetTokens().
Severity Justification:
-
Loss of Funds: Protocol distributes more Sepolia ETH than budgeted
-
Easy to Exploit: No special setup required; occurs naturally with normal usage
-
Immediate Impact: Each repeat claimer's transaction resets the counter mid-day
Real World Scenario:
If dailySepEthCap = 0.5 ETH and 100 first time users claim (50 users → 0.25 ETH), then 1 repeat claimer claims (counter resets to 0), then 50 more first time users can claim again (0.25 ETH). Total: 0.5 ETH distributed but counter only shows 0.25 ETH.
Proof of Concept
The vulnerability exists in RaiseBoxFaucet.sol at lines 185-213:
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;
}
} else {
dailyDrips = 0;
}
The Problem: Line 212 unconditionally resets dailyDrips = 0 when:
-
A user has already claimed ETH before (hasClaimedEth[user] == true), OR
-
ETH drips are paused (sepEthDripsPaused == true)
This resets the global daily counter in the middle of a day, breaking the cap mechanism.
Foundry Test Demonstrating the Exploit:
Add this test to your test suite:
function testDailyEthCapBypassViaCounterReset() public {
address Favour = makeAddr("Favour");
address Jolah = makeAddr("Jolah");
address Busayo = makeAddr("Busayo");
vm.prank(Jolah);
raiseBoxFaucet.claimFaucetTokens();
advanceBlockTime(block.timestamp + 3 days + 5 hours);
address dummy = makeAddr("dummy");
vm.prank(dummy);
raiseBoxFaucet.claimFaucetTokens();
vm.prank(Favour);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterFavour = raiseBoxFaucet.dailyDrips();
assertEq(dailyDripsAfterFavour, 0.01 ether);
vm.prank(Jolah);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterJolah = raiseBoxFaucet.dailyDrips();
assertEq(dailyDripsAfterJolah, 0);
vm.prank(Busayo);
raiseBoxFaucet.claimFaucetTokens();
uint256 totalEthDistributed = dummy.balance + Favour.balance + Busayo.balance;
uint256 counterValue = raiseBoxFaucet.dailyDrips();
assertTrue(totalEthDistributed > counterValue);
}
Test Output:
Total ETH distributed TODAY: 15000000000000000 [0.015 ETH]
dailyDrips counter (final): 5000000000000000 [0.005 ETH]
Tools Used
Recommended Mitigation Steps
Remove the else block that incorrectly resets the counter:
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) {
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;
}
The dailyDrips counter should only be reset when a new day begins , not when repeat claimers or paused drips occur.