Raisebox Faucet

First Flight #50
Beginner FriendlySolidity
100 EXP
Submission Details
Impact: low
Likelihood: medium

Off-by-one balance check prevents claiming when faucet balance equals faucetDrip

Author Revealed upon completion

Root + Impact

Description


Normal behavior: When the faucet holds exactly faucetDrip tokens, a user should be allowed to claim those tokens (the faucet should transfer the faucetDrip amount and drop to zero).

Actual behavior: The contract checks the faucet balance using <= faucetDrip and reverts when the faucet balance is equal to faucetDrip, preventing legitimate claims of the final available drip amount and effectively locking those tokens.

// Root cause in the codebase with @> marks to highlight the relevant section
// Faucet balance check before transferring tokens
if (balanceOf(address(this)) <= faucetDrip) {
@> revert RaiseBoxFaucet_InsufficientContractBalance();
}

Risk

Likelihood:

This will occur any time the faucet balance is exactly equal to faucetDrip (e.g., after funding the faucet with a multiple of the drip amount or when one final drip remains). Because funding and drip sizes are often round numbers, the condition is likely to occur in normal operational scenarios.

Impact

1.Users cannot claim a drip when the faucet holds exactly faucetDrip tokens; the faucet becomes unusable until the owner mints or transfers more tokens (even when there are exactly enough tokens to satisfy one claim).

2.This is a denial-of-service for legitimate claimers and may confuse testers or validators expecting the faucet to fully dispense its supply

Proof of Concept:

Fund the faucet with exactly faucetDrip tokens (e.g., transfer 1000 * 10**18 tokens if that is faucetDrip). A normal user calls claimFaucetTokens() and the transaction reverts with RaiseBoxFaucet_InsufficientContractBalance(). After the revert, the faucet still holds the same faucetDrip amount and no claim succeeds until the owner increases the contract balance beyond faucetDrip. This demonstrates the off-by-one check prevents dispensing the exact remaining drip.


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/RaiseBoxFaucet.sol";
contract FaucetEdgeCaseTest is Test {
RaiseBoxFaucet faucet;
address user = address(0xBEEF);
function setUp() public {
faucet = new RaiseBoxFaucet(
"RaiseBox Token",
"RBT",
1000 ether, // faucetDrip
0.005 ether, // sepEthDrip
1 ether // dailySepEthCap
);
vm.deal(address(faucet), 1 ether);
vm.deal(user, 1 ether);
}
function test_CannotClaimWhenBalanceEqualsDripAmount() public {
// drain contract to exactly 1 faucetDrip
uint256 drip = faucet.faucetDrip();
uint256 total = faucet.getFaucetTotalSupply();
faucet.mintFaucetTokens(address(faucet), drip); // ensure exact drip remains
// simulate user claiming
vm.prank(user);
vm.expectRevert(); // should NOT revert, but it does because of <= check
faucet.claimFaucetTokens();
}
}

}

Recommended Mitigation:

Fix: Change the comparison to allow equality. Replace <= with < (or use balance < faucetDrip).

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

Support

FAQs

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