Raisebox Faucet

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

[M-02] - Missing ReentrancyGuard in `claimFaucetTokens` violates CEI pattern

Author Revealed upon completion

Root + Impact

Description

The claimFaucetTokens function performs an external call to transfer tokens without implementing reentrancy protection. While the function has a cooldown mechanism, it violates the Checks-Effects-Interactions (CEI) pattern by updating state after the external call.

The specific issue is the absence of a ReentrancyGuard modifier combined with state updates occurring after the external token transfer, creating a potential attack vector.

// Root cause in the codebase
function claimFaucetTokens() external {
// @> No ReentrancyGuard modifier
// Checks
uint256 currentDay = block.timestamp / 1 days;
require(
currentDay > lastFaucetDripDay[msg.sender] + 2,
"Cooldown period not met"
);
// External Interaction (before Effects)
faucetToken.transfer(msg.sender, faucetDrip); // @> External call
// Effects (after Interaction - violates CEI)
lastFaucetDripDay[msg.sender] = currentDay; // @> State update after external call
faucetClaimer = msg.sender; // @> State update after external call
}

Risk

Likelihood:

  • Requires malicious token contract or callback mechanism

  • Cooldown provides partial mitigation but not complete protection

  • Attack complexity is moderate

  • Violates security best practices

Impact:

  • Potential for reentrancy exploitation

  • State inconsistency during execution

  • Violation of established security patterns

  • Risk increases if token contract is upgradeable or malicious

Proof of Concept

This test demonstrates the CEI pattern violation and lack of reentrancy protection:

  1. Setup: We deploy the faucet with a standard token

  2. Observation: The function performs an external call before state updates

  3. Risk: If the token has a callback, state can be manipulated

The vulnerability exists because:

  • No ReentrancyGuard modifier is present

  • External call happens before state updates

  • Cooldown check alone is insufficient protection

  • Violates Checks-Effects-Interactions pattern

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
import {RaiseBoxToken} from "../src/RaiseBoxToken.sol";
contract ReentrancyRiskTest is Test {
RaiseBoxFaucet faucet;
RaiseBoxToken token;
address owner = makeAddr("owner");
address user = makeAddr("user");
function setUp() public {
vm.startPrank(owner);
token = new RaiseBoxToken();
faucet = new RaiseBoxFaucet(address(token));
token.mintFaucetTokens(address(faucet), 1_000_000 * 10**18);
vm.stopPrank();
}
function testCEIPatternViolation() public {
// First claim to set lastFaucetDripDay
vm.prank(user);
faucet.claimFaucetTokens();
// Wait 3 days
vm.warp(block.timestamp + 3 days);
// Record state before claim
uint256 lastDripBefore = faucet.lastFaucetDripDay(user);
// Claim again - external call happens before state update
vm.prank(user);
faucet.claimFaucetTokens();
// State is updated after external call (CEI violation)
uint256 lastDripAfter = faucet.lastFaucetDripDay(user);
assertTrue(lastDripAfter > lastDripBefore);
// This demonstrates the pattern violation:
// 1. External call (transfer) happens first
// 2. State updates happen after
// 3. No ReentrancyGuard protection
}
}

Recommended Mitigation

Implement OpenZeppelin's ReentrancyGuard and follow the Checks-Effects-Interactions pattern by moving state updates before external calls. This prevents reentrancy attacks and follows security best practices.

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract RaiseBoxFaucet is Ownable {
+ contract RaiseBoxFaucet is Ownable, ReentrancyGuard {
- function claimFaucetTokens() external {
+ function claimFaucetTokens() external nonReentrant {
uint256 currentDay = block.timestamp / 1 days;
require(
currentDay > lastFaucetDripDay[msg.sender] + 2,
"Cooldown period not met"
);
+ // Effects (before Interaction)
+ lastFaucetDripDay[msg.sender] = currentDay;
+ faucetClaimer = msg.sender;
+ // Interaction (after Effects)
faucetToken.transfer(msg.sender, faucetDrip);
- lastFaucetDripDay[msg.sender] = currentDay;
- faucetClaimer = msg.sender;
}
}
Updates

Lead Judging Commences

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