Raisebox Faucet

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

Off-By-One Error in Balance Check Permanently Locks Last Claimable Tokens

Description

The claimFaucetTokens() function should allow claims when the faucet has sufficient balance. However, it uses <= instead of < at line 184, causing claims to fail when the balance exactly equals the drip amount.

The function reverts when the faucet balance equals the drip amount, permanently locking the last claimable tokens in the contract.

function claimFaucetTokens() public {
// ... checks ...
if (balanceOf(address(this)) <= faucetDrip) { // @> VULNERABILITY: Uses <= instead of <
revert RaiseBoxFaucet_InsufficientContractBalance();
}
// ... rest of function ...
}

Risk

Likelihood: High

  • Occurs naturally when balance depletes to exactly one drip amount

  • Deterministic - will happen with mathematical certainty during normal operation

  • Example: Faucet with 2000 tokens → user1 claims 1000 → 1000 tokens stuck forever

Impact: Medium

  • One drip amount (1000 tokens) permanently locked per faucet lifecycle

  • Last eligible user denied their rightful claim

  • Poor user experience - balance visible but unclaimable

  • Requires owner intervention to rescue stuck tokens

Proof of Concept

function testOffByOneErrorWastesLastClaimableAmount() public {
uint256 drip = 1000 * 10 ** 18;
// Setup faucet with exactly 2000 tokens (2 drips)
RaiseBoxFaucet smallFaucet = new RaiseBoxFaucet("raiseboxtoken", "RB", drip, 0.005 ether, 0.5 ether);
uint256 totalSupply = smallFaucet.getFaucetTotalSupply();
vm.prank(address(smallFaucet));
smallFaucet.transfer(address(this), totalSupply - (drip * 2));
vm.deal(address(smallFaucet), 1 ether);
vm.warp(3 days);
// User1 claims 1000 tokens successfully
vm.prank(user1);
smallFaucet.claimFaucetTokens();
assertEq(smallFaucet.getFaucetTotalSupply(), drip, "1000 tokens remain");
assertEq(smallFaucet.getBalance(user1), drip, "User1 got 1000 tokens");
// User2 tries to claim the last 1000 tokens but fails
vm.warp(block.timestamp + 1 days);
vm.prank(user2);
vm.expectRevert(RaiseBoxFaucet.RaiseBoxFaucet_InsufficientContractBalance.selector);
smallFaucet.claimFaucetTokens();
// 1000 tokens permanently stuck
assertEq(smallFaucet.getFaucetTotalSupply(), drip, "Tokens stuck forever");
}

Output:

Faucet balance: 1000 tokens
Drip amount: 1000 tokens
Balance == Drip? true
User2 claim: FAILED
Result: 1000 tokens permanently stuck

Recommended Mitigation

- if (balanceOf(address(this)) <= faucetDrip) {
+ if (balanceOf(address(this)) < faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}

Updates

Lead Judging Commences

inallhonesty Lead Judge 5 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.