Raisebox Faucet

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

[M-03] - CEI pattern violation in `claimFaucetTokens` via ETH transfer

Root + Impact

Description

The claimFaucetTokens function performs an ETH transfer to the caller using a low-level call without reentrancy protection. The function updates the dailyDrips counter after the external call, violating the Checks-Effects-Interactions (CEI) pattern.

The specific issue is that the ETH transfer (external call) happens before the state update of dailyDrips, allowing a malicious contract to re-enter and potentially exploit the state inconsistency.

// Root cause in the codebase
function claimFaucetTokens() external {
// ... token transfer ...
if (dailyDrips < maxDailyDrips) {
(bool success, ) = msg.sender.call{value: faucetDrip}(""); // @> External call
require(success, "ETH transfer failed");
dailyDrips++; // @> State update AFTER external call (CEI violation)
}
}

Risk

Likelihood:

  • Attacker can deploy malicious contract with receive/fallback function

  • No special conditions required beyond contract deployment

  • Single transaction exploit possible

  • Attack can be repeated until daily limit reached

Impact:

  • Potential for reentrancy to manipulate state

  • dailyDrips counter can be bypassed

  • Multiple claims possible before state update

  • Violation of CEI pattern creates attack surface

Proof of Concept

This test demonstrates the CEI pattern violation with ETH transfer:

  1. Setup: We deploy a malicious contract that can receive ETH

  2. Attack: The malicious contract calls claimFaucetTokens

  3. Vulnerability: ETH transfer happens before dailyDrips increment

The exploit works because:

  • External call (ETH transfer) happens first

  • State update (dailyDrips++) happens after

  • Malicious contract can execute code during the call

  • No ReentrancyGuard prevents re-entry

// 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 MaliciousReceiver {
RaiseBoxFaucet public faucet;
uint256 public callCount;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
receive() external payable {
// This code executes BEFORE dailyDrips is incremented
callCount++;
// Could potentially re-enter here if not for cooldown
// Demonstrates the CEI violation
}
function attack() external {
faucet.claimFaucetTokens();
}
}
contract ReentrancyETHTest is Test {
RaiseBoxFaucet faucet;
RaiseBoxToken token;
MaliciousReceiver attacker;
address owner = makeAddr("owner");
function setUp() public {
vm.startPrank(owner);
token = new RaiseBoxToken();
faucet = new RaiseBoxFaucet(address(token));
token.mintFaucetTokens(address(faucet), 1_000_000 * 10**18);
vm.deal(address(faucet), 100 ether);
vm.stopPrank();
attacker = new MaliciousReceiver(address(faucet));
}
function testCEIViolationWithETHTransfer() public {
uint256 dailyDripsBefore = faucet.dailyDrips();
// Attack: Malicious contract receives ETH before state update
attacker.attack();
uint256 dailyDripsAfter = faucet.dailyDrips();
// Verify CEI violation:
// 1. ETH was transferred to attacker (external call executed)
assertTrue(address(attacker).balance > 0);
// 2. Attacker's receive function was called BEFORE dailyDrips update
assertEq(attacker.callCount(), 1);
// 3. dailyDrips was incremented AFTER the external call
assertEq(dailyDripsAfter, dailyDripsBefore + 1);
// This demonstrates the CEI pattern violation with ETH transfer
}
}

Recommended Mitigation

Implement ReentrancyGuard and follow the Checks-Effects-Interactions pattern by updating dailyDrips before the external ETH transfer. This prevents reentrancy attacks during the ETH transfer callback.

+ import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
- contract RaiseBoxFaucet is Ownable {
+ contract RaiseBoxFaucet is Ownable, ReentrancyGuard {
- function claimFaucetTokens() external {
+ function claimFaucetTokens() external nonReentrant {
// ... token transfer ...
if (dailyDrips < maxDailyDrips) {
+ // Effects (before Interaction)
+ dailyDrips++;
+ // Interaction (after Effects)
(bool success, ) = msg.sender.call{value: faucetDrip}("");
require(success, "ETH transfer failed");
- dailyDrips++;
}
}
}

Alternative mitigation using pull pattern to eliminate external calls entirely:

mapping(address => uint256) public pendingETHWithdrawals;
function claimFaucetTokens() external nonReentrant {
// ... token transfer ...
if (dailyDrips < maxDailyDrips) {
dailyDrips++;
pendingETHWithdrawals[msg.sender] += faucetDrip;
}
}
function withdrawETH() external nonReentrant {
uint256 amount = pendingETHWithdrawals[msg.sender];
require(amount > 0, "No ETH to withdraw");
pendingETHWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "ETH transfer failed");
}
Updates

Lead Judging Commences

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

Give us feedback!