Raisebox Faucet

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

Cooldown updates after external call allow reentry

Root + Impact

Delayed cooldown updates leave claimFaucetTokens vulnerable to reentrancy that bypasses daily rate limits and cooldown enforcement.

Description

  • In normal operation the faucet should mark the caller’s lastClaimTime and increment dailyClaimCount before any external interaction so cooldown checks immediately reflect the new claim.

  • The function currently performs the ETH drip first, then updates lastClaimTime and dailyClaimCount, so a re-entering contract sees stale counters and passes the cooldown guard multiple times in one transaction.

if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
...
@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
@> if (!success) revert RaiseBoxFaucet_EthTransferFailed();
...
@> lastClaimTime[faucetClaimer] = block.timestamp;
@> dailyClaimCount++;

Risk

Likelihood:

  • Whenever the claimer is a contract with a payable fallback, the fallback can recursively call claimFaucetTokens before lastClaimTime mutates.

  • Flash-bot adversaries monitor the mempool for faucet claims and sandwich them with crafted contracts that immediately re-enter upon receiving the drip.

Impact:

  • Cooldown bypass allows the attacker to drain a disproportionate share of ETH drips in a single block.

  • Daily claim quotas lose integrity, enabling either permanent DoS for honest users or inflation of faucet token distribution.

Proof of Concept

The PoC shows a contract that re-enters during the ETH callback before lastClaimTime is written, so each loop passes the cooldown guard.

function receive() external payable {
if (address(faucet).balance >= sepEthAmountToDrip) {
faucet.claimFaucetTokens(); // still passes cooldown guard
}
}

Recommended Mitigation

Adopting this patch ensures lastClaimTime and dailyClaimCount update before the transfer, keeping cooldown checks effective during callbacks.

--- a/src/RaiseBoxFaucet.sol
+++ b/src/RaiseBoxFaucet.sol
@@ -148,6 +148,23 @@ contract RaiseBoxFaucet is ERC20, Ownable {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
+ /**
+ *
+ * @param lastFaucetDripDay tracks the last day a claim was made
+ * @notice resets the @param dailyClaimCount every 24 hours
+ */
+ if (block.timestamp > lastFaucetDripDay + 1 days) {
+ lastFaucetDripDay = block.timestamp;
+ dailyClaimCount = 0;
+ }
+
+ // Effects - ALL state updates MUST happen BEFORE any external calls
+
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
+
+ // Interactions - External calls happen LAST
+
// 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;
@@ -172,22 +189,6 @@ contract RaiseBoxFaucet is ERC20, Ownable {
dailyDrips = 0;
}
- /**
- *
- * @param lastFaucetDripDay tracks the last day a claim was made
- * @notice resets the @param dailyClaimCount every 24 hours
- */
- if (block.timestamp > lastFaucetDripDay + 1 days) {
- lastFaucetDripDay = block.timestamp;
- dailyClaimCount = 0;
- }
-
- // Effects
-
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
-
- // Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy in `claimFaucetTokens`

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.