Raisebox Faucet

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

Missing supply cap and per-mint limits creates centralization risk for token economics

Root + Impact

Root Cause: The mintFaucetTokens() function at lines 108-121 validates only that the contract's current balance is not above 1000 tokens but imposes no limits on the amount parameter. This allows the owner to mint unlimited tokens once the faucet balance drops below the threshold, creating a centralization risk.

While owner minting privileges are expected per specification ("owner can mint new tokens in future"), the complete absence of supply caps represents a design concern rather than a traditional security vulnerability.

Impact:

  • Centralization risk: Single point of trust in owner key

  • Unlimited supply potential: No hardcoded emission limits

  • Economic policy unclear: Users may expect controlled supply

  • Mistake amplification: Script errors could cause over-minting

Likelihood: Low - Requires owner privileges. External attackers cannot exploit. Only relevant if owner is malicious, compromised, or makes deployment mistakes.

Description

  • Normal behavior: The mintFaucetTokens() function allows the owner to mint tokens to the faucet when its balance falls below 1000 tokens, with the intent to refill supply for distribution to users.


  • Issue: While the function checks that balance is below 1000, it places no limit on the amount parameter. The owner can mint unlimited tokens in a single call once this threshold is met. The check can also be repeatedly bypassed via burn-mint cycles: burn excess tokens, mint any amount, repeat. This creates unlimited supply potential with no on-chain caps, making token economics depend entirely on owner trustworthiness rather than coded constraints.

// Vulnerable code in mintFaucetTokens() - Lines 109-121
function mintFaucetTokens(address to, uint256 amount) public onlyOwner {
// Check 1: Must mint to contract only
if (to != address(this)) {
revert RaiseBoxFaucet_MiningToNonContractAddressFailed();
}
// Check 2: Only validates current balance, not amount
if (balanceOf(address(to)) > 1000 * 10 ** 18) {
revert RaiseBoxFaucet_FaucetNotOutOfTokens();
}
// @> ISSUE: No validation on amount parameter
// @> Owner can mint unlimited tokens when balance < 1000
_mint(to, amount);
emit MintedNewFaucetTokens(to, amount);
}

Risk

Likelihood:

  • Owner privilege required: This issue manifests when the owner (or anyone controlling the owner private key) executes the mint function with the faucet balance below 1000 tokens

  • Bypass scenario: The owner burns tokens to reduce balance below threshold, then mints any desired amount, creating repeated inflation cycles

  • Accidental activation: Deployment scripts or automated refill mechanisms could trigger large mints without proper amount validation, causing unintended hyperinflation

  • Key compromise: When the owner key is compromised, the attacker gains ability to mint unlimited supply with no additional constraints

Impact:

  • Unlimited token supply: Total supply can be increased arbitrarily with no hardcoded maximum, destroying token scarcity assumptions

  • Token value dilution: Existing token holders experience value dilution as supply inflates without proportional utility increase

  • Economic model breakdown: The faucet's distribution mechanism becomes meaningless when supply is effectively infinite

  • Centralization transparency: Users relying on the faucet may not be aware that token supply is uncapped and entirely dependent on owner behavior rather than coded constraints

  • Trust dependency: Protocol economics rely entirely on owner restraint rather than on-chain guarantees, creating single point of failure for token integrity

Proof of Concept

The following Foundry test suite demonstrates the unlimited minting capability through three scenarios:

  1. Direct unlimited mint: Owner mints 1 billion tokens in a single transaction when faucet balance is below threshold

  2. Burn-mint cycle bypass: Repeated inflation via burning tokens to reset threshold, then minting again

  3. Access control verification: Confirms only owner can execute, not external attackers

All tests pass.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import "forge-std/Test.sol";
import {RaiseBoxFaucet} from "../src/RaiseBoxFaucet.sol";
/**
* @title UnlimitedMint_ByDesign_PoC
* @notice Minimal PoC demonstrating owner-only unlimited minting as a policy risk.
* @dev This is a centralization/tokenomics risk by design, not an external exploit.
*/
contract UnlimitedMint_ByDesign_PoC is Test {
// ----------- Constants -----------
uint256 constant FAUCET_DRIP = 1000 ether;
uint256 constant SEP_ETH_PER_USER = 0.01 ether;
uint256 constant DAILY_CAP = 0.1 ether;
uint256 constant MINT_THRESHOLD = 1000 ether;
uint256 constant LARGE_MINT = 1_000_000_000 ether; // 1B tokens
// ----------- State -----------
RaiseBoxFaucet faucet;
address owner;
// Mirror faucet event for expectEmit
event MintedNewFaucetTokens(address indexed user, uint256 amount);
function setUp() public {
faucet = new RaiseBoxFaucet("RB","RB", FAUCET_DRIP, SEP_ETH_PER_USER, DAILY_CAP);
owner = faucet.getOwner();
// Prepare faucet balance below threshold so mint is allowed by current design
uint256 cur = faucet.getFaucetTotalSupply();
if (cur > MINT_THRESHOLD) {
uint256 toBurn = cur - (MINT_THRESHOLD / 2);
vm.prank(owner);
faucet.burnFaucetTokens(toBurn);
}
assertLt(faucet.getFaucetTotalSupply(), MINT_THRESHOLD, "pre: faucet balance < threshold");
}
/// @notice Owner can mint an arbitrary amount when pool is below threshold (by design).
function test_OwnerCanMintArbitraryAmount_ByDesign() public {
uint256 beforeSupply = faucet.totalSupply();
uint256 beforeBalance = faucet.getFaucetTotalSupply();
vm.expectEmit(true, false, false, true);
emit MintedNewFaucetTokens(address(faucet), LARGE_MINT);
vm.prank(owner);
faucet.mintFaucetTokens(address(faucet), LARGE_MINT);
uint256 afterSupply = faucet.totalSupply();
uint256 afterBalance = faucet.getFaucetTotalSupply();
assertEq(afterBalance, beforeBalance + LARGE_MINT, "faucet balance inflated");
assertEq(afterSupply, beforeSupply + LARGE_MINT, "total supply inflated");
}
/// @notice The threshold check can be bypassed repeatedly via burn->mint cycles.
function test_MintGuardBypassable_ViaBurnMintCycle() public {
uint256 beforeSupply = faucet.totalSupply();
// Cycle 1: mint
vm.prank(owner);
faucet.mintFaucetTokens(address(faucet), 1_000_000 ether);
// Burn to push faucet balance below threshold again
vm.prank(owner);
faucet.burnFaucetTokens(500_000 ether);
// Cycle 2: mint again
vm.prank(owner);
faucet.mintFaucetTokens(address(faucet), 1_000_000 ether);
// Net effect: supply increased significantly, showing repeated inflation is trivial
uint256 afterSupply = faucet.totalSupply();
assertGt(afterSupply, beforeSupply + 1_000_000 ether, "net inflation via cycles");
}
/// @notice Only owner can mint; external attackers cannot directly exploit.
function test_AccessControl_OnlyOwnerCanMint() public {
address attacker = address(0xBAD);
vm.expectRevert(abi.encodeWithSignature("OwnableUnauthorizedAccount(address)", attacker));
vm.prank(attacker);
faucet.mintFaucetTokens(address(faucet), 10_000 ether);
}
}

Test execution results:

forge test --match-path test/UnlimitedMint_ByDesign_PoC.t.sol -vvv

✓ test_OwnerCanMintArbitraryAmount_ByDesign (gas: 53451)

  • Initial supply: 500 tokens (after setUp burn)

  • After mint: 1,000,000,500 tokens

  • Result: 1 billion tokens minted in single transaction

✓ test_MintGuardBypassable_ViaBurnMintCycle (gas: 68832)

  • Cycle 1: Mint 1M tokens

  • Burn 500k tokens to reset threshold

  • Cycle 2: Mint another 1M tokens

  • Result: Net 1.5M token inflation via repeated bypass

✓ test_AccessControl_OnlyOwnerCanMint (gas: 13126)

  • External attacker reverted with OwnableUnauthorizedAccount

  • Result: Confirms owner-only privilege (not external exploit)

Suite result: 3 passed, 0 failed

Key findings:

  • Unlimited minting is possible when owner controls the key

  • Threshold check is bypassable via burn-mint cycles

  • External attackers cannot exploit (access control works)

  • This is a centralization risk

The tests prove that token supply depends entirely on owner behavior with no on-chain caps or safeguards.


Recommended Mitigation

To improve tokenomics transparency and add safeguards against owner mistakes or key compromise, implement supply policy controls:

  1. Define maximum total supply cap

  2. Set per-mint transaction limit

  3. Validate amount parameter before minting

  4. Check post-mint balance to enforce caps

These changes maintain owner's ability to refill the faucet while preventing unlimited inflation and clarifying economic expectations for users.

+ // Add supply policy constants
+ uint256 public constant MAX_TOTAL_SUPPLY = 10_000_000 * 1e18; // 10M tokens max
+ uint256 public constant MAX_MINT_PER_CALL = 10_000 * 1e18; // 10K per transaction
+ uint256 public constant MAX_FAUCET_BALANCE = 5_000_000 * 1e18; // 5M faucet cap
function mintFaucetTokens(address to, uint256 amount) public onlyOwner {
if (to != address(this)) {
revert RaiseBoxFaucet_MiningToNonContractAddressFailed();
}
+ // Validate amount is within per-call limit
+ require(amount <= MAX_MINT_PER_CALL, "Mint amount exceeds per-call limit");
+
+ // Ensure total supply won't exceed maximum cap
+ require(
+ totalSupply() + amount <= MAX_TOTAL_SUPPLY,
+ "Mint would exceed maximum total supply"
+ );
+
+ // Ensure faucet balance won't exceed maximum
+ require(
+ balanceOf(address(this)) + amount <= MAX_FAUCET_BALANCE,
+ "Mint would exceed maximum faucet balance"
+ );
- // Remove insufficient old check
- if (balanceOf(address(to)) > 1000 * 10 ** 18) {
- revert RaiseBoxFaucet_FaucetNotOutOfTokens();
- }
_mint(to, amount);
emit MintedNewFaucetTokens(to, amount);
}
Updates

Lead Judging Commences

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