Raisebox Faucet

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

Claim fails even when exactly enough tokens left

Wrong <= check blocks claims at exact faucetDrip balance, denying users their tokens despite sufficient balance

Description

  • The RaiseBoxFaucet::claimFaucetTokens function checks if the contract has enough tokens before letting users claim their 1000-token drip every 3 days. Makes sense — line 175 reverts if the balance is too low.

  • But the check balanceOf(address(this)) <= faucetDrip is backwards. When there's exactly 1000 tokens left (enough for one claim), it still reverts! Should only block if less than 1000. Another off-by-one bug — users get screwed right when there's just enough, leaving the last claim hanging.

    function claimFaucetTokens() public {
    // ...
    @> if (balanceOf(address(this)) <= faucetDrip) { // Blocks even at exactly 1000
    revert RaiseBoxFaucet_InsufficientContractBalance();
    }
    // ...
    }

Risk

Likelihood: Medium

  • Hits when the balance gets low — happens naturally as users claim over time. Not rare in a busy faucet.

Impact: Medium

  • Denies users their legit 1000 tokens when there's exactly enough — straight-up breaks the "1000 per claim" promise

  • Frustrates users who see "insufficient balance" but know there's money there

  • Wastes the owner's time manually topping up sooner than needed

Proof of Concept

  • Test sets huge faucetDrip (100M) so 9 claims leave exactly 100M — should work for 10th user, but doesn't.

  • Add to RaiseBoxFaucet.t.sol:

    function test__ClaimDeniedEvenIfTokensPresentInContract() public {
    // Setup
    // Keeping the value of `faucetDrip` to be high, for testing purposes
    RaiseBoxFaucet raiseBox = new RaiseBoxFaucet(
    "raiseBoxFaucet",
    "RBF",
    100000000 * 10 ** 18, // faucetDrip
    0.005 ether,
    1 ether
    );
    console.log("Initial faucet token balance:", raiseBox.getBalance(address(raiseBox)) / 1e18, "tokens");
    console.log("Each user can claim:", raiseBox.faucetDrip() / 1e18, "tokens");
    // 9 users went for their claim
    for (uint i = 0; i < 9; i++) {
    address user = address(uint160(i+1));
    vm.prank(user);
    raiseBox.claimFaucetTokens();
    }
    console.log();
    console.log("Faucet token balance after 9 users made their claim:", raiseBox.getBalance(address(raiseBox)) / 1e18, "tokens");
    console.log("Exactly", raiseBox.faucetDrip() / 1e18, "tokens are left, one more user can still claim (maybe?!)");
    // Last claim failed
    vm.prank(user10);
    vm.expectRevert();
    raiseBox.claimFaucetTokens();
    console.log();
    console.log("Claim Denied!!");
    }

  • Run the above test using the following command:

    forge test --mt test__ClaimDeniedEvenIfTokensPresentInContract -vv

  • Logs:

    Ran 1 test for test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
    [PASS] test__ClaimDeniedEvenIfTokensPresentInContract() (gas: 2953549)
    Logs:
    Initial faucet token balance: 1000000000 tokens
    Each user can claim: 100000000 tokens
    Faucet token balance after 9 users made their claim: 100000000 tokens
    Exactly 100000000 tokens are left, one more user can still claim (maybe?!)
    Claim Denied!!
    Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.64ms (2.12ms CPU time)

Recommended Mitigation

Swap <= to < — only block if truly under faucetDrip, let the exact balance through.

function claimFaucetTokens() public {
// ...
- if (balanceOf(address(this)) <= faucetDrip) {
+ if (balanceOf(address(this)) < faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
// ...
}
Updates

Lead Judging Commences

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

Off-by-one error in `claimFaucetTokens` prevents claiming when the balance is exactly equal to faucetDrip

Support

FAQs

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