Raisebox Faucet

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

Incorrect `dailyDrips` Reset Breaks ETH Cap Mechanism

Root + Impact

The dailyDrips counter incorrectly resets to zero whenever a user who has already claimed ETH makes another token claim, or when ETH drips are paused. This completely bypasses the daily ETH distribution cap, allowing unlimited ETH to be distributed per day.

Description

  • The normal and expected behavior is that dailyDrips should only be reset to zero when a new day begins (tracked by comparing currentDay > lastDripDay). The variable should accumulate throughout the day to properly enforce the dailySepEthCap limit on total ETH distributed.

  • The critical bug occurs on lines 210-212 in an else block that executes whenever hasClaimedEth[faucetClaimer] is true (returning user) OR sepEthDripsPaused is true (drips are paused). This else block incorrectly resets dailyDrips = 0, which breaks the daily cap tracking mechanism. Every time a user who has already received their ETH drip comes back to claim tokens (after the 3-day cooldown), the daily ETH counter is reset, allowing more ETH to be distributed than the configured cap.

if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0; // Correct: Reset on new day
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
// ... ETH transfer logic ...
}
@>} else {
@> dailyDrips = 0; // BUG: Incorrectly resets for returning users or when paused!
@>}

Risk

Likelihood:

  • This bug triggers every time a user who has already claimed their ETH drip comes back to claim tokens

  • After initial claims, most users will be returning users (after the 3-day cooldown)

  • The bug occurs constantly during normal protocol operations with no special conditions required

  • It's deterministic and reproducible 100% of the time

Impact:

  • The daily ETH cap (dailySepEthCap = 1 ETH) becomes completely ineffective and is bypassed

  • Multiple times more ETH than intended can be distributed per day (potentially unlimited)

  • The protocol's economic security model is fundamentally broken

  • The contract could drain its entire ETH balance much faster than the owner intended

  • First-time users can continue claiming ETH well beyond the daily limit

  • Owner loses control over the ETH distribution rate

Proof of Concept

This test demonstrates how the daily ETH cap is completely bypassed due to incorrect reset logic when returning users claim tokens.

it("Should demonstrate how daily ETH cap is bypassed", async function () {
console.log("\n=== Starting Daily ETH Cap Bypass Test ===");
console.log(`Daily ETH Cap: ${ethers.formatEther(DAILY_ETH_CAP)} ETH`);
console.log(`ETH per User: ${ethers.formatEther(SEP_ETH_DRIP)} ETH`);
console.log(`Expected Max Users per Day: ${EXPECTED_MAX_USERS_PER_DAY}\n`);
let totalEthDistributed = 0n;
let firstTimeClaimers = 0;
// First 180 users claim (still under cap)
console.log("Phase 1: First 180 users claim (under cap)");
for (let i = 0; i < 180; i++) {
await faucet.connect(users[i]).claimFaucetTokens();
firstTimeClaimers++;
totalEthDistributed += SEP_ETH_DRIP;
}
let dailyDripsValue = await faucet.dailyDrips();
console.log(`Daily Drips after 180 users: ${ethers.formatEther(dailyDripsValue)} ETH`);
console.log(`Total ETH distributed: ${ethers.formatEther(totalEthDistributed)} ETH\n`);
// Next 20 users should reach the cap (200 total = 1 ETH)
console.log("Phase 2: Next 20 users claim (should reach cap)");
for (let i = 180; i < 200; i++) {
await faucet.connect(users[i]).claimFaucetTokens();
firstTimeClaimers++;
totalEthDistributed += SEP_ETH_DRIP;
}
dailyDripsValue = await faucet.dailyDrips();
console.log(`Daily Drips after 200 users: ${ethers.formatEther(dailyDripsValue)} ETH`);
console.log(`Total ETH distributed: ${ethers.formatEther(totalEthDistributed)} ETH`);
console.log(`\u2705 Cap SHOULD be reached now (1 ETH distributed)\n`);
// User 201 should be rejected (cap reached)
console.log("Phase 3: User 201 tries to claim (should fail - cap reached)");
await faucet.connect(users[200]).claimFaucetTokens();
// Note: They get tokens but NO ETH (skipped with event)
console.log("⚠️ User 201 got tokens but no ETH (cap reached)\n");
// Now a RETURNING USER claims (BUG TRIGGERS HERE)
console.log("Phase 4: 🐛 BUG TRIGGER - User 1 returns after 3 days");
await ethers.provider.send("evm_increaseTime", [3 * 24 * 60 * 60 + 1]); // Fast forward 3 days
await ethers.provider.send("evm_mine");
console.log("User 1 (returning user) claims tokens again...");
await faucet.connect(users[0]).claimFaucetTokens();
dailyDripsValue = await faucet.dailyDrips();
console.log(`\u274c BUG: Daily Drips reset to: ${ethers.formatEther(dailyDripsValue)} ETH (should be 1 ETH!)\n`);
// Now MORE users can claim ETH even though cap was reached!
console.log("Phase 5: 🚨 EXPLOIT - More users claim ETH beyond the cap");
for (let i = 201; i < 220; i++) {
await faucet.connect(users[i]).claimFaucetTokens();
firstTimeClaimers++;
totalEthDistributed += SEP_ETH_DRIP;
}
dailyDripsValue = await faucet.dailyDrips();
console.log(`Daily Drips after exploitation: ${ethers.formatEther(dailyDripsValue)} ETH`);
console.log(`Total first-time claimers: ${firstTimeClaimers} (should be max 200)`);
console.log(`Total ETH distributed: ${ethers.formatEther(totalEthDistributed)} ETH`);
console.log(`\u274c Daily cap of ${ethers.formatEther(DAILY_ETH_CAP)} ETH was BYPASSED!`);
// Verify the cap was exceeded
expect(totalEthDistributed).to.be.greaterThan(DAILY_ETH_CAP);
expect(firstTimeClaimers).to.be.greaterThan(EXPECTED_MAX_USERS_PER_DAY);
console.log("\n❗ CONCLUSION: The daily ETH cap mechanism is completely broken!");
});
});

Recommended Mitigation

The fix is simple: remove the incorrect else block that resets dailyDrips. The variable should only be reset when a new day begins, which is already handled correctly in the existing day boundary check.

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;
}
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.