A token, part 3: caps and burning
A maximum supply is a promise. Enforce it in code, because that is the only place anyone can verify it.
A token that claims a fixed supply and enforces it only in its README has not promised anything. The cap belongs in mint:
abi.require(_uget(K_TOTAL) + amt <= _uget(K_MAX), b"cap_exceeded")
Burning is the reverse of minting: take units out of the caller's balance and reduce total supply by the same amount.
Burn must reduce total supply. A burn that deducts a balance without lowering the total leaves the contract claiming more tokens exist than the sum of all balances. Nothing breaks immediately — which is why this bug survives into production and then makes every downstream calculation subtly wrong.
A cap of 0 conventionally means "uncapped" in the Animica token standard. That is a design decision you should state in your ABI documentation, because the alternative reading — a token that can never mint anything — is equally plausible to someone reading only the code.
Your turn
Add a cap and burning. init(max_supply) sets the cap (0 means uncapped). mint must reject anything that would exceed it with b"cap_exceeded". burn(amount) removes the caller's tokens and lowers total supply.
Hints
One way to do it
from stdlib import abi, events, storage
def _k_bal(addr: bytes) -> bytes:
return b"tok:bal:" + addr
def _uget(key: bytes) -> int:
raw = storage.get(key, b"")
return 0 if raw == b"" else int.from_bytes(raw, "big")
def _uset(key: bytes, value: int) -> None:
v = int(value)
abi.require(v >= 0, b"negative")
if v == 0:
storage.delete(key)
return
storage.set(key, v.to_bytes(max(1, (v.bit_length() + 7) // 8), "big"))
K_TOTAL = b"tok:total"
K_MAX = b"tok:max"
def init(max_supply: int) -> None:
_uset(K_MAX, int(max_supply))
def total_supply() -> int:
return _uget(K_TOTAL)
def max_supply() -> int:
return _uget(K_MAX)
def balance_of(addr: bytes) -> int:
return _uget(_k_bal(bytes(addr)))
def mint(to: bytes, amount: int) -> int:
amt = int(amount)
abi.require(amt > 0, b"must_be_positive")
cap = _uget(K_MAX)
total = _uget(K_TOTAL)
# A cap of 0 means uncapped, by convention.
if cap > 0:
abi.require(total + amt <= cap, b"cap_exceeded")
dest = bytes(to)
_uset(_k_bal(dest), _uget(_k_bal(dest)) + amt)
_uset(K_TOTAL, total + amt)
events.emit(b"Mint", {"to": dest, "value": amt})
return _uget(K_TOTAL)
def burn(amount: int) -> int:
amt = int(amount)
abi.require(amt > 0, b"must_be_positive")
src = abi.caller()
have = _uget(_k_bal(src))
abi.require(have >= amt, b"insufficient")
_uset(_k_bal(src), have - amt)
_uset(K_TOTAL, _uget(K_TOTAL) - amt)
events.emit(b"Burn", {"from": src, "value": amt})
return _uget(K_TOTAL)
Claim your 10 ANM
Finish this lesson and claim 10 ANM, once per address. Paid from the Animica treasury in batches — allow a few minutes.
Stuck? Ask
Answered by Animica's own free inference network. It is donated GPU capacity, so give it 20-30 seconds.