Raisebox Faucet

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

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

Author Revealed upon completion

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();
}

Support

FAQs

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