Raisebox Faucet

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

Reentrancy in claimFaucetTokens enables double token claims

Author Revealed upon completion
# Reentrancy in `claimFaucetTokens` enables double token claims
## Description
The faucet stores `msg.sender` to the state variable `faucetClaimer`, performs multiple eligibility checks, and then attempts to drip Sepolia ETH with `faucetClaimer.call{value: sepEthAmountToDrip}("")`. Because state tracking (`lastClaimTime`, `dailyClaimCount`, token transfers) happens **after** that external call, the contract violates the Checks-Effects-Interactions pattern. A malicious claimer can re-enter `claimFaucetTokens` from its `receive`/`fallback` before the cooldown or daily counters are updated, effectively claiming again within the same transaction.
## Risk
- **Severity:** High / Critical
- Reentrancy lets an attacker bypass the 3-day cooldown and `dailyClaimLimit`, draining multiple token drips in one transaction.
- The re-entry also interacts with the daily ETH accounting bug, enabling repeated resets of `dailyDrips` so subsequent users can claim ETH beyond the daily cap.
- The faucet can lose its entire token stock and ETH buffer.
## Proof of Concept
Deploy an attacker contract that claims once and re-enters during the Sepolia ETH drip:
```solidity
contract FaucetAttacker {
RaiseBoxFaucet immutable faucet;
bool internal reentered;
constructor(RaiseBoxFaucet _faucet) {
faucet = _faucet;
}
function attack() external {
faucet.claimFaucetTokens();
// attacker now holds faucetDrip twice and the ETH drip once
}
receive() external payable {
if (!reentered) {
reentered = true;
faucet.claimFaucetTokens(); // cooldown & counters still pass
}
}
}
```
1. `attack()` calls `claimFaucetTokens()`.
2. Faucet sends the ETH drip to the attacker contract before updating any state.
3. `receive()` triggers, re-enters `claimFaucetTokens()`, passes all checks again, and obtains a second drip.
4. Original call resumes, updates counters once, and transfers another token drip—netting two drips in one transaction while daily limits are under-counted.
## Recommended Mitigation
Adopt strict CEI and guard against reentrancy:
```solidity
function claimFaucetTokens() external nonReentrant {
address claimant = msg.sender;
_enforceEligibility(claimant);
// update all state before interactions
lastClaimTime[claimant] = block.timestamp;
dailyClaimCount++;
if (shouldDripEth(claimant)) {
hasClaimedEth[claimant] = true;
_updateDailyDrips();
}
_transfer(address(this), claimant, faucetDrip); // send tokens first
if (shouldDripEth(claimant)) {
(bool success, ) = claimant.call{value: sepEthAmountToDrip}("");
if (!success) revert RaiseBoxFaucet_EthTransferFailed();
}
}
```
- Replace the storage assignment `faucetClaimer = msg.sender` with a local variable.
- Update cooldown counters, `dailyClaimCount`, and any ETH-specific state before performing `call`.
- Transfer tokens before dripping ETH to avoid reentrancy on token logic.
- Optionally add OpenZeppelin’s `ReentrancyGuard` for defense in depth.

Support

FAQs

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