Raisebox Faucet

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

Unconditional daily ETH counter reset enables daily-cap bypass and excess Sepolia ETH distribution

Unconditional daily ETH counter reset enables daily-cap bypass and excess Sepolia ETH distribution

Description

  • The faucet should drip Sepolia ETH to first-time claimers while respecting a daily ETH cap (dailySepEthCap). The cumulative amount dripped in a day (dailyDrips) must never exceed this cap.

  • In the else path (i.e., when the caller is not a first-time claimer or drips are paused), the code resets the global dailyDrips to 0. This lets any caller who already claimed ETH (or when drips are paused) erase the daily usage, enabling subsequent first-time claimers to receive ETH beyond the intended daily cap.

function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
.
.
.
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip ? "Faucet out of ETH" : "Daily ETH cap reached"
);
}
} else {
@> dailyDrips = 0;
}

Risk

Likelihood:

  • During any call by a non-first-time claimer (or when drips are paused), execution reaches the else branch, which always resets dailyDrips to zero.

  • After such a reset, the next first-time claimer(s) within the same day will receive ETH as if no ETH had been dripped, breaching the daily cap.

Impact:

  • Exceeds daily ETH cap: The faucet distributes more ETH per day than configured, breaking operator assumptions and accounting.

  • Abuse amplification: An attacker (or just many returning users) can repeatedly trigger resets to allow unbounded daily distribution to new claimers.

Proof of Concept

The PoC configures a small drip (0.01) and small daily cap (0.02). Two first-time claimers (user1, user2) legitimately consume the entire daily cap (total 0.02). Then, a non-first-time claimer (user1 again) calls claimFaucetTokens(), hitting the else { dailyDrips = 0; } branch and resetting the counter. A third first-time claimer (user3) then receives ETH despite the cap already being reached. The final assertion confirms total dripped ETH exceeds the daily cap.

function testCanBypassDailyEthDripLimit() public {
vm.startPrank(owner);
// deploy with small sepEth drip and small cap for easy testing
raiseBoxFaucet = new RaiseBoxFaucet("RaiseBox", "RBF", 1 ether, 0.01 ether, 0.02 ether);
vm.stopPrank();
// fund contract with some ETH to allow drips
vm.deal(owner, 1 ether);
vm.prank(owner);
(bool sent,) = payable(address(raiseBoxFaucet)).call{value: 0.1 ether}("");
require(sent, "funding failed");
// user1 is first-time claimer -> should receive sepEthAmountToDrip (0.01)
vm.startPrank(user1);
// move time forward so cooldown checks pass
skip(4 days);
raiseBoxFaucet.claimFaucetTokens();
vm.stopPrank();
// Ensure user1 got the ETH
assertEq(user1.balance, 0.01 ether);
// user2 is first-time claimer -> should receive 0.01 but cap is 0.02, so both should be allowed normally
vm.startPrank(user2);
skip(4 days);
raiseBoxFaucet.claimFaucetTokens();
vm.stopPrank();
assertEq(user2.balance, 0.01 ether);
// At this point dailyDrips should be 0.02 (cap reached)
// Now call claimFaucetTokens again as alice (hasClaimedEth == true). According to the code
// execution will hit the `else { dailyDrips = 0; }` resetting the counter. After that a new
// first-time claimant could still get dripped ETH even though the cap was reached.
// user1 calls again to trigger the reset
vm.startPrank(user1);
skip(4 days);
raiseBoxFaucet.claimFaucetTokens();
vm.stopPrank();
// Now user3 claims; because dailyDrips was reset to 0 by user1's call, user3 will receive ETH
vm.startPrank(user3);
skip(4 days);
raiseBoxFaucet.claimFaucetTokens();
vm.stopPrank();
// user3 got ETH despite cap previously being reached
assertEq(user3.balance, 0.01 ether, "user3 should have received ETH after reset");
// Summed dripped ETH to users should be > dailySepEthCap if reset occurred
uint256 totalDripped = user1.balance + user2.balance + user3.balance;
assertGt(totalDripped, 0.02 ether, "Total dripped exceeded daily cap due to reset");
}

Recommended Mitigation

Reset dailyDrips only at the day boundary, not on the non-eligible path. Remove the unconditional reset and centralize daily resets under a single “day” clock.

function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
.
.
.
}
} 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 14 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.