Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Daily Claim Counter Fails to Reset Due to Misplaced Logic

RaiseBoxFaucet::dailyClaimCount Never Resets, Permanently Blocking New Token Claims After Limit Exceeded

Description

  • The RaiseBoxFaucet contract has a limit on how many users can claim tokens in a day, which is implemented using the combination of RaiseBoxFaucet::dailyClaimCount and RaiseBoxFaucet::dailyClaimLimit. The contract makes sure that dailyClaimCount must reset to 0 once every day, so that new users can claim tokens again.

  • However, the contract fails to implement the reset logic for dailyClaimCount in the right manner. After the limit is reached, no new user can claim tokens ever again, because RaiseBoxFaucet::claimFaucetTokens reverts before even reaching to the point where the reset happens. It doesn't matter whether one day has passed or more than that; the dailyClaimCount never resets until the owner manually increases the dailyClaimLimit using RaiseBoxFaucet::adjustDailyClaimLimit.

    function claimFaucetTokens() public {
    // ...
    @> if (dailyClaimCount >= dailyClaimLimit) {
    @> revert RaiseBoxFaucet__DailyClaimLimitReached(); // Reverts here before reaching to the reset logic
    }
    // ...
    if (block.timestamp > lastFaucetDripDay + 1 days) {
    lastFaucetDripDay = block.timestamp;
    @> dailyClaimCount = 0; // This line is never reached after the limit is reached
    }
    // ...
    @> dailyClaimCount++; // Increments dailyClaimCount
    }

Risk

Likelihood: High

  • The limit can be exhausted rapidly by coordinated sybil attacks (e.g., a single entity spinning up multiple addresses) or organic growth in a new protocol's user base, making this trigger inevitable in a live deployment.

Impact: High

  • Permanently excludes legitimate new users from onboarding tokens, halting protocol adoption and leading to widespread frustration.

  • Forces manual owner intervention, introducing centralisation risks and operational overhead that could delay recovery for days or weeks.

  • In a testnet faucet like this, it undermines the entire purpose of incentivising early participation, potentially causing reputational damage and lost TVL.

Proof of Concept

  • Here's how the vulnerability unfolds (as shown in the PoC test case below):

    1. The owner deploys the RaiseBoxFaucet contract and fulfils the initial token and Sepolia ETH donation requirements.

    2. The owner adjusts the dailyClaimLimit to a low number, let's say 2, for testing purposes.

    3. Two different users claim tokens successfully, reaching the daily limit.

    4. A third user tries to claim tokens but fails because the daily limit has been reached.

    5. One day passes, and the third user hopes that they can claim tokens now, but they still can't because the dailyClaimCount never resets to 0.

  • Add this test case to the existing RaiseBoxFaucet.t.sol file:

    function test__UserCanNeverClaimTokensAfterDailyClaimLimitReached() public {
    // Setup
    vm.prank(owner); // Ensuring the owner is the one deploying the contract
    RaiseBoxFaucet raiseBox = new RaiseBoxFaucet(
    "raiseBoxFaucet",
    "RBF",
    1000 * 10 ** 18,
    0.005 ether,
    1 ether
    ); // Daily claim limit is 100
    console.log("Daily claim limit on deployment:", raiseBox.dailyClaimLimit());
    // Decreasing the daily claim limit to 2 for testing purposes
    vm.prank(owner);
    raiseBox.adjustDailyClaimLimit(98, false); // New daily claim limit is 2
    console.log("Daily claim limit adjusted to:", raiseBox.dailyClaimLimit());
    // User 1 claims successfully
    vm.prank(user1);
    raiseBox.claimFaucetTokens();
    console.log();
    console.log("User1 claims successfully.");
    console.log("Current daily claim count:", raiseBox.dailyClaimCount());
    // User 2 claims successfully
    vm.prank(user2);
    raiseBox.claimFaucetTokens();
    console.log();
    console.log("User2 claims successfully.");
    console.log("Current daily claim count:", raiseBox.dailyClaimCount());
    // User 3 tries to claim but should fail as the daily limit is reached
    vm.prank(user3);
    vm.expectRevert();
    raiseBox.claimFaucetTokens(); // This will revert
    console.log();
    console.log("User3 tries to claim but the daily limit is reached.");
    // Fast forwarding the time by 1 day, so the daily claim count resets
    advanceBlockTime(block.timestamp + 1 days + 1 seconds); // Adding 1 second to ensure we are past the 24-hour mark
    console.log();
    console.log("Fast forwarding time by 1 day...");
    console.log("1 day has passed, daily claim count should reset (hopefully)");
    // User 3 tries to claim again after 1 day, but it reverts again
    vm.prank(user3);
    vm.expectRevert();
    raiseBox.claimFaucetTokens(); // This will revert
    console.log();
    console.log("User3 tries to claim again, but it reverts again.");
    console.log("Current daily claim count is still:", raiseBox.dailyClaimCount());
    }

  • Run the above test using the following command:

    forge test --mt test__UserCanNeverClaimTokensAfterDailyClaimLimitReached -vv

  • Logs:

    Ran 1 test for test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
    [PASS] test__UserCanNeverClaimTokensAfterDailyClaimLimitReached() (gas: 2558679)
    Logs:
    Daily claim limit on deployment: 100
    Daily claim limit adjusted to: 2
    User1 claims successfully.
    Current daily claim count: 1
    User2 claims successfully.
    Current daily claim count: 2
    User3 tries to claim but the daily limit is reached.
    Fast forwarding time by 1 day...
    1 day has passed, daily claim count should reset (hopefully)
    User3 tries to claim again, but it reverts again.
    Current daily claim count is still: 2
    Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.04ms (374.55µs CPU time)

Recommended Mitigation

The contract should implement the reset logic for dailyClaimCount in a way that it resets to 0 once every day, regardless of whether the daily limit has been reached or not. This can be done by moving the reset logic to the very beginning of the claimFaucetTokens function, so that it always resets before checking the daily limit.

function claimFaucetTokens() public {
// ...
+ if (block.timestamp > lastFaucetDripDay + 1 days) {
+ lastFaucetDripDay = block.timestamp;
+ dailyClaimCount = 0; // Reset dailyClaimCount at the start of the function
+ }
if (dailyClaimCount >= dailyClaimLimit) {
revert RaiseBoxFaucet__DailyClaimLimitReached();
}
// ...
- if (block.timestamp > lastFaucetDripDay + 1 days) {
- lastFaucetDripDay = block.timestamp;
- dailyClaimCount = 0;
- }
// ...
dailyClaimCount++;
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 11 days ago
Submission Judgement Published
Validated
Assigned finding tags:

dailyClaimCount Reset Bug

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.