Raisebox Faucet

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

Reentrancy Allows Double Claim in Single Transaction in `RaiseBoxFaucet::claimFaucetTokens`

Root + Impact

Description

In claimFaucetTokens, qualifying first-time claims check the daily ETH cap and balance, update hasClaimedEth and dailyDrips, then perform the ETH transfer via (bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}(""). If the recipient is a malicious contract, its receive() can reenter claimFaucetTokens. The inner call passes checks (e.g., cooldown uses the same timestamp, passing for new claims) and executes the token transfer (_transfer), emitting events, but skips ETH drip due to hasClaimedEth already set. The outer call then completes, performing another token transfer.

This results in two token claims per transaction (outer and inner), bypassing the intent of one claim per cooldown period. While ETH drip is limited (inner skips it), the double token issuance erodes the faucet's supply and daily count integrity. The vulnerability stems from the call's position after state updates but before final interactions, allowing unchecked reexecution.

// @> Root cause in the codebase
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
// @> Reentrancy point: Call after partial state change
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
}
// Later: Second _transfer occurs in outer call after reentry
_transfer(address(this), faucetClaimer, faucetDrip);

Risk

Likelihood:

  • Medium: Requires a malicious contract as claimant (common in attacks), but no complex setup beyond deploying and calling.

  • Cooldown and state updates limit depth, but single reentry is sufficient for abuse.

Impact:

  • Medium: Enables double token claims per transaction, accelerating supply depletion and partially bypassing daily limits, leading to faster faucet exhaustion.

  • No ETH double-dip (inner skips drip), but repeated attacks could drain tokens, denying service to legitimate users.

Proof of Concept

The following Foundry test deploys a malicious contract that reenters claimFaucetTokens during receive(). The outer call updates state and transfers ETH, triggering reentry. The inner call skips ETH but performs a token transfer, followed by the outer's token transfer, resulting in two claims in one transaction.

Add the following to the RaiseBoxFaucetTest.t.sol test, including the MaliciousClaimer contract:

Proof of Code
function test_Reentrancy() public {
MaliciousClaimer malicious = new MaliciousClaimer(raiseBoxFaucet);
// ... assume + deploy malicious
vm.prank(address(malicious));
raiseBoxFaucet.claimFaucetTokens(); // Expect no infinite loop
uint256 maliciousTokenBalance = raiseBoxFaucet.getBalance(address(malicious));
console.log("Malicious Claimer Balance:", maliciousTokenBalance);
uint256 maliciousEthBalance = address(malicious).balance;
console.log("Malicious Claimer Eth Balance:", maliciousEthBalance);
uint256 dailyClaim = raiseBoxFaucet.dailyClaimCount();
console.log("Daily Claim Count:", dailyClaim);
assertEq(dailyClaim, 2);
}
contract MaliciousClaimer {
RaiseBoxFaucet faucet;
constructor(RaiseBoxFaucet _faucet) {
faucet = _faucet; }
receive() external payable {
faucet.claimFaucetTokens(); // Reenter
}
}

Explanation

  • Setup: Deploys MaliciousClaimer with the faucet address and calls claimFaucetTokens as the malicious contract.

  • Outer Call: Updates hasClaimedEth and dailyDrips, then transfers ETH, triggering receive().

  • Reentrant Call: receive() re-calls claimFaucetTokens. The inner call skips ETH (due to hasClaimedEth=true) but passes cooldown (same timestamp) and performs a token transfer, incrementing dailyClaimCount to 1.

  • Outer Continuation: Completes with another token transfer, incrementing dailyClaimCount to 2, and emits SepEthDripped for the ETH.

  • Result: The malicious contract receives 2 token transfers (2e21 tokens) and 1 ETH drip (5e15 wei) in one transaction, confirming double claim without cooldown enforcement. The test passes, proving the vulnerability allows unauthorized double issuance.

Recommended Mitigation

Introduce OpenZeppelin's ReentrancyGuard modifier on claimFaucetTokens to prevent reentry, or move the ETH transfer after all state updates and use Address.sendValue for safer external calls. Alternatively, perform the token transfer before the ETH call to limit reentry impact.

+ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
+ contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard { // Add inheritance
- contract RaiseBoxFaucet is ERC20, Ownable, {
// In claimFaucetTokens
- function claimFaucetTokens() public {
+ function claimFaucetTokens() public nonReentrant { // Add modifier
// ... existing logic ...
(bool 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.