Raisebox Faucet

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

Reentrancy via ETH drip in claimFaucetTokens

Root + Impact

Reentrancy window during the ETH faucet drip lets an attacker re-enter claimFaucetTokens and drain funds before cooldown variables are updated.

Description

  • Under normal conditions the faucet drips Sepolia ETH to first-time claimants, then records the claim time and increments the per-day counters before handing out ERC20 tokens.

  • Because the function performs the external call{value: ...} before timestamp and counter updates, a malicious contract can regain execution control while the faucet still reflects the pre-claim state.

function claimFaucetTokens() public {
...
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
@> if (!success) revert RaiseBoxFaucet_EthTransferFailed();
@> lastClaimTime[faucetClaimer] = block.timestamp;
@> dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
}

Risk

Likelihood:

  • Reentrancy occurs whenever the faucet sends ETH to a contract whose receive() function performs another claim before lastClaimTime changes.

  • The cooldown check still passes on re-entry because the stale lastClaimTime and dailyClaimCount values are read again during the same transaction.

Impact:

  • Multiple re-entrant calls can exhaust ETH held for drips, denying legitimate users.

  • The attacker can chain claims to bypass rate limiting and accumulate an outsized share of ERC20 faucet tokens.

Proof of Concept

This PoC deploys a malicious receiver whose fallback recursively calls claimFaucetTokens, consuming the ETH drip multiple times before the faucet updates cooldown state.

contract ReenteringClaimer {
RaiseBoxFaucet faucet;
uint256 calls;
function attack() external {
calls = 0;
faucet.claimFaucetTokens();
}
receive() external payable {
if (calls < 5) {
calls++;
faucet.claimFaucetTokens();
}
}
}

Recommended Mitigation

Install the state-update patch so cooldown variables change before the external call, eliminating the reentrancy window described above.

--- a/src/RaiseBoxFaucet.sol
+++ b/src/RaiseBoxFaucet.sol
@@ -146,6 +146,17 @@ 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 - Update state BEFORE any external calls
// drip sepolia eth to first time claimers if supply hasn't ran out or sepolia drip not paused**
// still checks
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
@@ -160,6 +171,10 @@ contract RaiseBoxFaucet is ERC20, Ownable {
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
+
+ // Update state variables BEFORE external call to prevent reentrancy
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
@@ -175,23 +190,16 @@ contract RaiseBoxFaucet is ERC20, Ownable {
}
} else {
dailyDrips = 0;
+ // Update state variables even when no ETH drip occurs
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
}
- /**
- *
- * @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
+ // Interactions - External calls happen AFTER all state updates
_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.