Mintable Token
Problem
I need a fungible token where an authorized owner can create new tokens, but total supply is capped.
Solution
token MintableToken {
name: "Mintable Token"
symbol: "MTK"
decimals: 18
supply: 0 tokens
initial_holder: deployer
field owner: address = deployer
field max_supply: amount = 100_000_000 tokens
event Minted(to: address indexed, amount: amount)
event OwnershipTransferred(prev: address indexed, next: address indexed)
error SupplyCapExceeded()
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 <= max_supply or revert_with SupplyCapExceeded()
balances[to] = balances[to] + amount
total_supply = total_supply + amount
emit Minted(to, amount)
}
action transfer_ownership(new_owner: address) only owner {
given new_owner != address(0) or revert_with ZeroAddress()
emit OwnershipTransferred(owner, new_owner)
owner = new_owner
}
}
Explanation
supply: 0 tokens— starts with zero supply; minting creates itonly owner— themintaction is gated by the owner guard- The three
givenchecks (in order): amount not zero, recipient not zero address, cap not exceeded transfer_ownershipuses a one-step transfer; see Variations for two-step
The compiler desugars only owner to given caller == owner or revert_with Unauthorized() and prepends it to the action body.
Gas Estimate
| Operation | Gas |
|---|---|
mint (cold recipient) | ~65,000 |
mint (warm recipient) | ~25,000 |
transfer_ownership | ~30,000 |
Common Pitfalls
- Missing cap check: Without
max_supply, an owner can inflate supply indefinitely. Always set a cap unless supply is intentionally unbounded. - No zero-address guard on mint recipient: Tokens sent to
address(0)are permanently burned without triggeringTransfer(addr, 0). - One-step ownership: A single
transfer_ownershipis irreversible if the new owner is wrong. See the two-step variation below. - Overflow:
total_supply + amountcould overflow without the cap check. The compiler catches this for constant values, but runtime values require an explicit check. - Minting to self: Minting to the owner’s own address is valid but may be unintended. Add a check if needed.
Variations
Unbounded supply (no cap)
Remove max_supply and the cap check if total supply is governed externally:
action mint(to: address, amount: amount) only owner {
given amount > 0 or revert_with ZeroAmount()
balances[to] = balances[to] + amount
total_supply = total_supply + amount
emit Minted(to, amount)
}
Two-step ownership transfer
field pending_owner: address = address(0)
action begin_transfer(new_owner: address) only owner {
pending_owner = new_owner
}
action accept_ownership() {
given caller == pending_owner or revert_with Unauthorized()
emit OwnershipTransferred(owner, caller)
owner = caller
pending_owner = address(0)
}