Raisebox Faucet

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

Owner can still mint tokens with exactly 1000 remaining

Owner Can Mint Tokens When Exactly 1000 Left, Bypassing "Not Out of Tokens" Check

Description

  • The RaiseBoxFaucet::mintFaucetTokens function is there for the owner to refill the contract's token balance when it's running low — specifically, it should only allow minting if the balance drops below 1000 tokens (the per-claim amount promised to users). That way, the faucet stays topped up without wasting mints.

  • But the check uses > 1000 * 10**18, which means if there's exactly 1000 tokens left (enough for one last claim), the owner can still mint more. This goes against the intent: you shouldn't need to refill until it's truly depleted. It's a classic off-by-one bug — a small slip, but it lets the owner pump extra tokens in prematurely, inflating the supply a bit more than needed each time.

    function mintFaucetTokens(address to, uint256 amount) public onlyOwner {
    // ...
    @> if (balanceOf(address(to)) > 1000 * 10 ** 18) { // Doesn't fail if exactly 1000 tokens are left
    revert RaiseBoxFaucet_FaucetNotOutOfTokens();
    }
    @> _mint(to, amount);
    // ...
    }

Risk

Likelihood: Low

  • The owner must actively call mintFaucetTokens, making accidental or malicious over-minting unlikely in a well-managed setup.

Impact: Low

  • Over-minting can lead to inflation of the faucet token supply, potentially diluting token value and undermining user trust. However, it does not directly affect contract security or user funds.

Proof of Concept

  • Here's a test that burns down to a workable balance, simulates claims to hit exactly 1000, then shows the owner minting anyway (when they shouldn't). Comments walk through the flow.

  • Add the test case to the RaiseBoxFaucet.t.sol file:

    function test__MintingAllowedEvenIf1000TokensExist() public {
    // Setup
    // Currently, the RaiseBoxFaucet will have an initial supply of 1 billion tokens, which won't be good for testing purposes
    // Thus, we gotta burn all those tokens first, and then mint around 10000 tokens for testing
    console.log("-----SETUP-----");
    console.log();
    vm.prank(owner);
    RaiseBoxFaucet raiseBox = new RaiseBoxFaucet(
    "raiseBoxFaucet",
    "RBF",
    1000 * 10 ** 18,
    0.005 ether,
    1 ether
    );
    console.log("Initial Faucet Token Supply:", raiseBox.getFaucetTotalSupply() / 10 ** 18, "Tokens");
    // Burning all the initial supply
    vm.prank(owner);
    raiseBox.burnFaucetTokens(raiseBox.getFaucetTotalSupply());
    console.log("Faucet Token Supply after burning all the initial supply:", raiseBox.getFaucetTotalSupply() / 10 ** 18, "Tokens");
    // Now, minting 10000 tokens to the contract itself
    vm.prank(owner);
    raiseBox.mintFaucetTokens(address(raiseBox), 10000 * 10 ** 18);
    console.log("Faucet Token Supply after minting 10000 tokens:", raiseBox.getFaucetTotalSupply() / 10 ** 18, "Tokens");
    // Now the real test begins
    // Owner can't mint more tokens as there are already enough tokens (10000) in the contract
    vm.prank(owner);
    vm.expectRevert();
    raiseBox.mintFaucetTokens(address(raiseBox), 1000 * 10 ** 18); // This will revert
    console.log();
    console.log("-----TEST BEGINS-----");
    console.log();
    console.log("Owner tries to mint another 1000 tokens, but it reverts as there are already enough tokens in the contract.");
    // Around 9 users claim their tokens, which will bring the token balance to 1000 tokens in the contract
    for (uint256 i = 0; i < 9; i++) {
    address user = address(uint160(i+1)); // Generating pseudo user addresses
    vm.prank(user);
    raiseBox.claimFaucetTokens();
    }
    console.log("Faucet Token Supply after 9 users claimed their tokens:", raiseBox.getFaucetTotalSupply() / 10 ** 18, "Tokens");
    // Still, 1000 tokens are left in the contract, and one user can still claim.
    // At this point, the contract has enough tokens, and the owner shouldn't be able to mint more. But, that's not the case, unfortunately...
    vm.prank(owner);
    raiseBox.mintFaucetTokens(address(raiseBox), 1000 * 10 ** 18); // This should revert, but it doesn't
    console.log("Faucet Token Supply after owner minted another 1000 tokens:", raiseBox.getFaucetTotalSupply() / 10 ** 18, "Tokens");
    }

  • Run the above test using the following command:

    forge test --mt test__MintingAllowedEvenIf1000TokensExist -vv

  • Logs:

    Ran 1 test for test/RaiseBoxFaucet.t.sol:TestRaiseBoxFaucet
    [PASS] test__MintingAllowedEvenIf1000TokensExist() (gas: 2980968)
    Logs:
    -----SETUP-----
    Initial Faucet Token Supply: 1000000000 Tokens
    Faucet Token Supply after burning all the initial supply: 0 Tokens
    Faucet Token Supply after minting 10000 tokens: 10000 Tokens
    -----TEST BEGINS-----
    Owner tries to mint another 1000 tokens, but it reverts as there are already enough tokens in the contract.
    Faucet Token Supply after 9 users claimed their tokens: 1000 Tokens
    Faucet Token Supply after owner minted another 1000 tokens: 2000 Tokens
    Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.14ms (532.68µs CPU time)

Recommended Mitigation

Easy to fix, just replace the > operator with >= operator in the check.

function mintFaucetTokens(address to, uint256 amount) public onlyOwner {
// ...
- if (balanceOf(address(to)) > 1000 * 10 ** 18) {
+ if (balanceOf(address(to)) >= 1000 * 10 ** 18) {
revert RaiseBoxFaucet_FaucetNotOutOfTokens();
}
_mint(to, amount);
// ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 18 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Appeal created

0xscratch Submitter
17 days ago
inallhonesty Lead Judge
15 days ago
inallhonesty Lead Judge 15 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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