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 {
faucetClaimer = msg.sender;
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();
}
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;
}
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 {
assertEq(raiseBoxFaucet.dailySepEthCap(), 0.5 ether);
assertEq(raiseBoxFaucet.sepEthAmountToDrip(), 0.005 ether);
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");
}
assertEq(raiseBoxFaucet.dailyDrips(), 0.5 ether, "Daily drips should equal cap");
address user101 = makeAddr("user101");
vm.prank(user101);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(user101).balance, 0 ether, "User 101 should NOT receive ETH - cap reached");
advanceBlockTime(block.timestamp + 3 days);
vm.prank(users[0]);
raiseBoxFaucet.claimFaucetTokens();
assertEq(raiseBoxFaucet.dailyDrips(), 0, "VULN: dailyDrips incorrectly reset!");
vm.prank(user101);
raiseBoxFaucet.claimFaucetTokens();
assertEq(address(user101).balance, 0.005 ether, "VULN: User 101 bypassed daily cap!");
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");
}
}
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.