Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

dailyDrips, in the claimFaucetTokens function, is incorrectly reset to 0 for any caller who already claimed ETH, breaking daily cap accounting and enabling dailySepEthCap bypass

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; // 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; <@ 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 {
// user1 makes the very first claim (first-time claimer => should drip 0.005 ETH)
vm.startPrank(user1);
raiseBoxFaucet.claimFaucetTokens();
console.log("first claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
// advance time so other users can claim (new day scenario is tracked internally by contract logic)
vm.warp(block.timestamp + 3 days);
// subsequent first-time claimers on the same day; dailyDrips should accumulate by 0.005 ETH per new first-time claimer
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();
// user1 claims again (has already received ETH before). In the buggy implementation, this path reset dailyDrips to 0.
vm.startPrank(user1);
raiseBoxFaucet.claimFaucetTokens();
console.log("reset claim:", raiseBoxFaucet.dailyDrips());
vm.stopPrank();
// another new first-time claimer after the reset—dailyDrips might start counting again from 0 (demonstrates the bug)
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;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 5 days ago
Submission Judgement Published
Validated
Assigned finding tags:

dailyDrips Reset Bug

Support

FAQs

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