Raisebox Faucet

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

Reentrancy - external call happens before critical state updates

Author Revealed upon completion

Reentrancy - external call happens before critical state updates

Description

In claimFaucetTokens() the contract makes an external call to send ETH to the claimer:

@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");

This external call occurs before the function updates lastClaimTime and dailyClaimCount. Although hasClaimedEth and dailyDrips are updated before the call, the other critical state variables are not. Because the external call transfers control to an EOA/contract, a malicious recipient contract can reenter claimFaucetTokens() and call it again before lastClaimTime and dailyClaimCount are changed. That allows bypassing cooldowns and daily limits and enables multiple token transfers from the faucet.

Risk

Likelihood: HIGH

Impact: HIGH

  • An attacker (contract) can perform reentrancy to drain faucet token balance (multiple token _transfer operations) and circumvent daily claim limits and cooldowns. This can lead to severe token loss, abuse of faucet, and unintended token distribution.

Proof of Concept

1. Deploy a malicious contract with payable fallback that calls `raiseBox.claimFaucetTokens()` from within the fallback.
2. Ensure first call triggers ETH send to the malicious contract; fallback reenters and calls `claimFaucetTokens()` again while `lastClaimTime` is still old and `dailyClaimCount` unchanged.
3. The malicious contract can repeat until token balance is drained or other checks fail.

Recommended Mitigation

Apply Check-Effects-Interactions: do all state updates that prevent reentry before the external call. Also make faucetClaimer a local variable (not a contract-level storage var), and add a reentrancy guard.

- // set faucetClaimer as state var
- faucetClaimer = msg.sender;
+ // use local variable
+ address faucetClaimerLocal = msg.sender;
...
- // Effects
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
-
- // Interactions
- _transfer(address(this), faucetClaimer, faucetDrip);
+ // Effects — update before any external calls or transfers
+ lastClaimTime[faucetClaimerLocal] = block.timestamp;
+ dailyClaimCount++;
+
+ // Interactions — safe: local variables & state already updated
+ _transfer(address(this), faucetClaimerLocal, faucetDrip);

Also add a reentrancy guard (OpenZeppelin ReentrancyGuard) to claimFaucetTokens:

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract RaiseBoxFaucet is ERC20, Ownable {
+ contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard {
...
- function claimFaucetTokens() public {
+ function claimFaucetTokens() public nonReentrant {

Support

FAQs

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