Then some lines after that , state updates as follows.
That is opening for reentrancy risk, see POC below.
A malicious attacker contract can be deployed that reenters during the ETH callback:
contract Attacker {
RaiseBoxFaucet public faucet;
constructor(address _faucet) {
faucet = RaiseBoxFaucet(_faucet);
}
function attack() external {
faucet.claimFaucetTokens();
}
receive() external payable {
if (address(faucet).balance >= 0.01 ether) {
faucet.claimFaucetTokens();
}
}
}
function testReentrancy() public {
Attacker attacker = new Attacker(address(faucet));
vm.expectRevert();
attacker.attack();
}
+ Move the Interactions after state updates (_transfer() (ERC20) then .call{value:}
function claimFaucetTokens() public {
address faucetClaimer = msg.sender;
//
if (
faucetClaimer == address(0) ||
faucetClaimer == address(this) ||
faucetClaimer == Ownable.owner()
) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
// Reset daily counters if 24h has passed
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
//
// Record that this address has claimed tokens
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// Mark ETH drip as claimed here (if applicable), *before* sending ETH
bool shouldDripEth = !hasClaimedEth[faucetClaimer] && !sepEthDripsPaused;
if (shouldDripEth) {
hasClaimedEth[faucetClaimer] = true;
}
//
// 1. Send faucet tokens <<=
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(faucetClaimer, faucetDrip);
// 2. Drip Sepolia ETH (if allowed and balance available) <<=
if (shouldDripEth) {
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
if (
dailyDrips + sepEthAmountToDrip <= dailySepEthCap &&
address(this).balance >= sepEthAmountToDrip
) {
dailyDrips += sepEthAmountToDrip;
(bool success, ) = faucetClaimer.call{value: sepEthAmountToDrip}("");
if (!success) revert RaiseBoxFaucet_EthTransferFailed();
emit SepEthDripped(faucetClaimer, sepEthAmountToDrip);
} else {
emit SepEthDripSkipped(
faucetClaimer,
address(this).balance < sepEthAmountToDrip
? "Faucet out of ETH"
: "Daily ETH cap reached"
);
}
}
}