Capped Supply Token
Problem
I need a fungible token with an absolute, permanently-fixed maximum supply — set at deployment and impossible to raise later, even by the owner.
Solution
token CappedToken {
name: "Capped Token"
symbol: "CAP"
decimals: 18
supply: 0 tokens
initial_holder: deployer
field owner: address = deployer
field immutable cap: amount = 21_000_000 tokens
event Minted(to: address indexed, amount: amount)
error CapExceeded()
error ZeroAmount()
error ZeroAddress()
action mint(to: address, amount: amount) only owner {
given amount > 0 or revert_with ZeroAmount()
given to != address(0) or revert_with ZeroAddress()
given total_supply + amount <= cap or revert_with CapExceeded()
balances[to] = balances[to] + amount
total_supply = total_supply + amount
emit Minted(to, amount)
}
}
Explanation
field immutable capis written once at deploy and lives in contract bytecode, not storage- The compiler replaces every
capread with the constant value — zeroSLOADcost given total_supply + amount <= capguarantees supply can never exceed the ceiling- Differs from Mintable Token: here the cap is truly permanent; in the mintable recipe
max_supplyis a mutable field the owner could change with an extra action
Because cap is immutable, even an owner-key compromise cannot raise it — the attacker can mint up to the cap but never past it.
Gas Estimate
| Operation | Gas |
|---|---|
| Deployment | ~480,000 |
mint (cold recipient) | ~62,000 |
mint (warm recipient) | ~22,000 |
cap read (inlined) | 0 |
Common Pitfalls
- Using
fieldinstead offield immutable: A regularfield capcosts 2,100 gas per read and can be silently mutated. Always useimmutablefor a hard cap. - Setting the cap too low: Since it cannot be raised, pick the cap conservatively. Migration requires deploying a new contract.
- Decimals confusion:
21_000_000 tokenswithdecimals: 18means21_000_000 * 10^18base units. Writing21_000_000without thetokenssuffix bypasses the scaling. - No burn path: Burned tokens free up cap room for future minting. If you want truly deflationary economics, also burn from
cap— butimmutableprevents that, so choose carefully.
Variations
Cap parameterised at deploy
constructor(_cap: amount) {
cap = _cap // written once, then immutable
}
Combined with Mintable + schedule
field immutable cap: amount = 1_000_000_000 tokens
field immutable mint_end: timestamp = deploy_time + 4 * 365 days
action mint(to: address, amount: amount) only owner {
given now < mint_end or revert_with MintingClosed()
given total_supply + amount <= cap or revert_with CapExceeded()
balances[to] = balances[to] + amount
total_supply = total_supply + amount
emit Minted(to, amount)
}