Raisebox Faucet

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

Off‑by‑one error in `claimFaucetTokens` prevents dispensing the last drip

Description

  • When a user calls claimFaucetTokens(), the faucet should allow a transfer as long as its token balance is at least one full drip (faucetDrip). If the balance is exactly equal to faucetDrip, the claim should succeed and empty the faucet.

  • The contract checks balanceOf(address(this)) <= faucetDrip and reverts in that case. This off‑by‑one condition blocks claims when the faucet balance is exactly faucetDrip, permanently stranding one full drip in the contract and causing an unexpected “insufficient balance” revert for users.

function claimFaucetTokens() public {
// ... other checks ...
// @> Off-by-one: this reverts when balance == faucetDrip (the last valid drip)
if (balanceOf(address(this)) <= faucetDrip) {
revert RaiseBoxFaucet_InsufficientContractBalance();
}
// ... later ...
_transfer(address(this), faucetClaimer, faucetDrip);
emit Claimed(msg.sender, faucetDrip);
}

Risk

Likelihood: Low

  • In normal operation, repeated claims reduce the faucet’s balance in steps of faucetDrip, so it regularly reaches exactly faucetDrip.

  • At that point, the next claimant will always hit this condition and revert, even though a full drip is available.

Impact: Low

  • Last‑drip lock: One full drip becomes permanently undistributable (“dust lock” at one‑drip granularity).

  • User experience degradation: Eligible claimers receive an unexpected InsufficientContractBalance revert and the operator may need manual intervention (e.g., mint/burn/top‑up) to continue distribution.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
interface IRaiseBoxFaucet {
function claimFaucetTokens() external;
function getFaucetTotalSupply() external view returns (uint256);
}
contract OffByOnePOC {
IRaiseBoxFaucet public faucet;
uint256 public faucetDrip; // assumed known from deployment params
constructor(IRaiseBoxFaucet _faucet, uint256 _faucetDrip) {
faucet = _faucet;
faucetDrip = _faucetDrip;
}
/// @dev Conceptual demonstration:
/// 1) Repeatedly claim until faucet balance == faucetDrip.
/// 2) Attempt one more claim -> reverts with InsufficientContractBalance due to <= check.
function demonstrate() external {
// --- Precondition (arranged in a test): faucet balance reduced to exactly faucetDrip ---
// assert(faucet.getFaucetTotalSupply() == faucetDrip);
// This final claim should be allowed, but the bug makes it revert.
// In a Foundry test: vm.expectRevert(RaiseBoxFaucet_InsufficientContractBalance.selector);
faucet.claimFaucetTokens(); // reverts because check uses <= instead of <
}
}

Recommended Mitigation

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

Lead Judging Commences

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