Root + Impact
Description
-
The dailyDrips counter is designed to track the total amount of Sepolia ETH dripped per day to enforce the dailySepEthCap limit.
-
The function incorrectly resets dailyDrips to 0 in the else branch when a user who has already claimed ETH calls claimFaucetTokens, completely breaking the daily ETH cap enforcement mechanism.
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 {
emit SepEthDripSkipped();
}
} else {
dailyDrips = 0;
}
Risk
Likelihood:
-
This occurs every time a user who has previously claimed ETH calls claimFaucetTokens again
-
This happens frequently in normal operation as users claim tokens multiple times
-
The bug activates immediately after the first user claims ETH a second time
Impact:
-
Daily ETH cap is completely bypassed and becomes ineffective
-
Contract can drip unlimited ETH per day, far exceeding dailySepEthCap
-
Contract ETH reserves will be depleted much faster than intended
-
Economic model of the faucet is broken
-
Malicious users can coordinate to drain ETH by alternating first-time and repeat claimers
Proof of Concept
function testDailyDripsResetBypass() public {
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.dailyDrips(), 0.005 ether);
vm.warp(block.timestamp + 4 days);
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.dailyDrips(), 0);
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
}
Recommended Mitigation
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;
}
Alternative: Move daily reset logic to the beginning of the function
+ // Reset counters at the start of the function
+ uint256 currentDay = block.timestamp / 24 hours;
+ if (currentDay > lastDripDay) {
+ lastDripDay = currentDay;
+ 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) {
// ... rest of logic
}
- } else {
- dailyDrips = 0;
}