ADR-018: Minting and Burning Tokens
| Status | Implemented |
| Date | 2026-04-22 |
Context
EBEMT is an e-money token: the issuer creates tokens against reserve deposits and retires them against redemptions. The current contract has no external mint or burn entry point — only the layer-crossing publicToEncryptedTransfer / encryptedToPublicTransfer operations, which move tokens between layers but do not change true issued supply. Tests use TestEBEMT.mintForTesting, explicitly marked "NOT FOR PRODUCTION."
ADR-006 defines how encryptedTotalSupply moves on every supply-changing operation and flags mint-to-encrypted as "if ever supported." This ADR closes that gap.
Both layers need first-class mint and burn: some users are onboarded directly into the encrypted layer without ever holding public tokens, and the issuer may hold encrypted positions on its own balance sheet that it needs to retire without a public-layer detour. Private mint is a qualitatively different trust surface from public mint: the amount is hidden, so an unauthorized mint is invisible on-chain in the amount dimension and is only detectable by the holder of totalSupplySK.
Proposal
Roles
One owner-maintained set and two owner-maintained authorization relations:
mapping(address => bool) public minters; // EVM addresses
// (burner, burnFrom) pairs: burner may burn from burnFrom, nothing else.
mapping(address => mapping(address => bool)) public publicBurnAuth; // (burner, EVM target)
mapping(address => mapping(CompressedPoint => bool)) public encryptedBurnAuth; // (burner, EPK target)
Burners are not required to be the owner of the account they burn from. Instead, the owner authorizes each (burner, burnFrom) pair explicitly, and a burner can only burn from the specific targets it has been authorized for. This decouples the operational key used to submit burn transactions from the balance-holding account, so issuer-held positions (public EVM addresses or encrypted EPKs sitting on the issuer's balance sheet) can be retired by a dedicated burner key without that key itself holding funds.
The split between publicBurnAuth and encryptedBurnAuth mirrors the two balance layers: public authorizations name an EVM target (the source of an ERC-20 balance), encrypted authorizations name an EPK target (the source of an encrypted balance). Authorizations are keyed on the EPK, not the controller, so rotating an EPK's controller does not change which (burner, EPK) pairs are eligible.
Admin functions, all onlyOwner: addMinter / removeMinter, addPublicBurnAuth(burner, burnFrom) / removePublicBurnAuth(burner, burnFrom), addEncryptedBurnAuth(burner, epk) / removeEncryptedBurnAuth(burner, epk). owner holds no mint or burn authority implicitly — it must enroll a dedicated operational address and authorize it against specific targets. This keeps the owner key (upgrade, verifier updates, key rotation) separate from the daily issuance keys. Revocation takes effect immediately.
Entry points
| Entry point | Authorization | Effect on encryptedTotalSupply |
|---|---|---|
publicMint(address ebemt, address to, uint256 amount) | minters[msg.sender] | Target EBEMT TS += amount (public delta) |
encryptedMint(address ebemt, epk, mintCt, tsCt, trcCt, proof) | minters[msg.sender] | Target EBEMT TS += tsCt (homomorphic) |
publicBurn(address ebemt, address burnFrom, uint256 amount) | publicBurnAuth[msg.sender][burnFrom], burns from burnFrom | Target EBEMT TS -= amount (public delta) |
encryptedBurn(address ebemt, epk, newBalance, tsCt, trcCt, clearPending, deactivatePending, proof) | encryptedBurnAuth[msg.sender][compressEpk(epk)] | Target EBEMT TS -= tsCt (homomorphic) |
All four entry points live on EBHub, take the target EBEMT as their first argument, and use plain msg.sender authorization. Minters and burners are administrative roles held by the issuer, so they can submit transactions directly and pay their own gas — the ERC-4337 / SharedAccount relayer path exists for user operations (where msg.sender is the SharedAccount), and bringing it into issuer operations would require EIP-712 signatures and nonce bitmaps for no functional benefit.
encryptedMint credits go through the existing _addToBalance / _creditSlot chokepoint, so the recipient EPK must already be registered and pending-balance routing is respected automatically. No pending flags are needed on mint (they are a sender-side concept).
encryptedBurn is a sender-side operation and takes the usual clearPending / deactivatePending booleans, bound to the proof via aux_commitment as every other balance-decreasing operation already does.
Encrypted total supply counter. For public mint/burn the amount is known, so the existing _addToTotalSupply(uint256) / _subFromTotalSupply(uint256) helpers apply unchanged. For private mint/burn the amount is hidden, so the circuit emits a ciphertext tsAmount = Enc(amount, totalSupplyPK, r) and the contract adds or subtracts it homomorphically — two new helpers, one pair of events.
Security notes
Minter compromise is the top failure mode. A compromised encryptedMint caller can inflate supply to any registered EPK, and the amount is hidden. The only reliable detector is the totalSupplySK holder periodically decrypting the counter and reconciling against an authorized-mint ledger; detection latency equals audit frequency. Mitigations are operational: a multisig (Safe) as the minter, separation from owner and burner keys, and out-of-band per-mint attestation.
Burner compromise is bounded by its authorization list. Each burner can only burn from (burner, burnFrom) pairs the owner has explicitly added, so a compromised burner destroys only balances the owner has designated as burnable through that key — typically issuer-held reserve accounts — never arbitrary user funds. The encrypted-burn authorization is keyed on specific EPKs, so granting (burner, epkA) does not confer authority over epkB even if both are controlled by the same address.
Rate limits: trivial for public mint, structurally hard for private mint. publicMint can gain a per-epoch cap (mintedInEpoch[epoch] + amount <= DAILY_CAP) in a few lines — the amount is in the clear. encryptedMint cannot be rate-limited at the contract level without access to totalSupplySK. Options short of that: a tighter per-transaction range bound in-circuit (e.g. amount < 2^40), a separate encrypted "minted-today" counter for auditor monitoring, or challenge-and-revoke driven by the auditor. None are adopted here — the ADR accepts this asymmetry and relies on multisig custody plus auditor monitoring as compensating controls.
Implementation also adds two ZK circuits (one per encrypted path), their verifiers, and admin setters mirroring the existing verifier setters.