Minting and Burning
EBEMT is an e-money token: the issuer creates tokens against reserve deposits and retires them against redemptions. Both the public ERC-20 layer and the encrypted layer expose first-class mint and burn entry points.
Every supply-changing operation updates the encrypted total supply counter; layer-crossing operations (publicToEncryptedTransfer, encryptedToPublicTransfer, autoEncrypt) do not.
Roles
One owner-maintained set and two owner-maintained authorization relations, all disjoint from the contract owner:
| Role mapping | Key | Guards |
|---|---|---|
minters | EVM address | publicMint, encryptedMint |
publicBurnAuth | (burner EVM address, burnFrom EVM address) | publicBurn(burnFrom, amount) |
encryptedBurnAuth | (burner EVM address, target compressed EPK) | encryptedBurn(epk, …) |
owner is NOT implicitly any of the above — it must enroll a dedicated operational address (or Safe multisig). This separates the upgrade / verifier-rotation key from the daily issuance keys.
Burn authorizations are pair-keyed: a burner is authorized only against the specific targets the owner has enrolled it for, never any target it controls or holds. This decouples the operational burner key from the balance-holding account, so issuer-held positions can be retired by a dedicated burner without that key itself holding funds. The split between publicBurnAuth (EVM target) and encryptedBurnAuth (EPK target) mirrors the two balance layers; encrypted authorizations are keyed on the EPK, not its controller, so rotating an EPK's controller does not change which (burner, EPK) pairs are eligible.
Role administration lives on EBHub. All three setters are onlyOwner on Hub; revocation takes effect immediately and applies to every EBEMT in the deployment, since the entry points read the role state from Hub on each call.
Hub.setMinter(address, bool)Hub.setPublicBurnAuth(address burner, address burnFrom, bool)Hub.setEncryptedBurnAuth(address burner, CompressedPoint epk, bool)
Entry points
All four mint/burn entry points live on EBHub and take the target EBEMT address as the first arg. Hub authorizes the caller, validates the EPK / amount, runs proof verification under Hub.totalSupplyPk, then calls the matching onlyHub executor on the EBEMT to mutate balance + counter state and emit per-token events.
| Hub function | Authorization | Counter effect |
|---|---|---|
Hub.publicMint(ebemt, to, amount) | minters[msg.sender] | TS += amount (public delta) |
Hub.publicBurn(ebemt, burnFrom, amount) | publicBurnAuth[msg.sender][burnFrom], burns from burnFrom | TS -= amount (public delta) |
Hub.encryptedMint(ebemt, epk, mintCt, tsCt, trcCt, proof) | minters[msg.sender] | TS += tsCt (homomorphic) |
Hub.encryptedBurn(ebemt, epk, newBalance, tsCt, trcCt, clearPending, deactivatePending, proof) | encryptedBurnAuth[msg.sender][compressEpk(epk)] | TS -= tsCt (homomorphic) |
Each call additionally requires isEbemt[ebemt] — the target must be a registered EBEMT in the deployment. Minters and burners are administrative roles held by the issuer; production issuance submits transactions directly from an issuer-controlled minter. The testnet faucet is the exception: it grants Hub.setMinter(SharedAccount, true) and routes fixed-size Hub.publicMint(ebemt, ...) calls through the ERC-4337 / SharedAccount path so test users do not need ETH. See Faucet.
Hub.encryptedMint dispatches to EBEMT.executeEncryptedMint, which credits the recipient EPK through the same _addToBalance / _creditSlot chokepoint used by encryptedTransfer and publicToEncryptedTransfer — so the recipient EPK must already be registered and pending-balance routing is respected automatically. No pending flags on mint — these are sender-side concepts.
Hub.encryptedBurn reads the EPK's current encrypted balance from the EBEMT (via encryptedBalanceOf) to bind it as a public input to the burn proof, runs verification, then dispatches to EBEMT.executeEncryptedBurn to commit the new balance and decrement the counter. The circuit pins the old balance ciphertext via the decryption check, so a replay after a successful burn is rejected naturally (state moved on).
ZK circuits
Two new Noir circuits, each generating a corresponding Solidity verifier:
eb-mint
Proves the mint preserves consistency between the recipient ciphertext, the total supply ciphertext, and the compliance ciphertext:
| Private inputs | amount, mint_randomness, ts_randomness, trc_randomness |
| Public inputs | recipient_EPK, mint_ciphertext, ts_pk, ts_ciphertext, trc_pk, trc_ciphertext |
| Constraints | mint_ciphertext = Enc(amount, recipient_EPK, mint_randomness); ts_ciphertext = Enc(amount, ts_pk, ts_randomness); trc_ciphertext = Enc(amount, trc_pk, trc_randomness) — the SAME amount under all three keys; amount < 2^64 |
No EPK-ownership check (the minter has no ESK for the recipient) and no balance-read (mint is purely additive).
eb-burn
Proves the burner owns the source EPK, knows the plaintext old balance, has sufficient balance, and that tsCt and trcCt both encode the same hidden burn_amount that was removed from the EPK's balance:
| Private inputs | sender_sk, sender_balance, burn_amount, new_randomness, ts_randomness, trc_randomness |
| Public inputs | sender_EPK, old_balance_ciphertext, new_balance_ciphertext, ts_pk, ts_ciphertext, trc_pk, trc_ciphertext |
| Constraints | sender_sk · G == sender_EPK; old_balance_ciphertext decrypts to sender_balance; sender_balance >= burn_amount; new_balance_ciphertext = Enc(sender_balance - burn_amount, sender_EPK, new_randomness); ts_ciphertext = Enc(burn_amount, ts_pk, ts_randomness); trc_ciphertext = Enc(burn_amount, trc_pk, trc_randomness); burn_amount < 2^64; new_balance < 2^64 |
No _aux_commitment in either circuit: authorization is by msg.sender against the role tables, so there is no EIP-712 signature or contract-supplied payload to bind to the proof.
Events
| Event | Purpose |
|---|---|
PublicMint(address indexed to, uint256 amount) | Emitted by publicMint. Amount is in the clear. |
PublicBurn(address indexed from, uint256 amount) | Emitted by publicBurn. Amount is in the clear. |
EncryptedMint(CompressedPoint indexed to, ElGamalCiphertext tsCiphertext, ElGamalCiphertext trcCiphertext) | Emitted by encryptedMint. tsCiphertext is the auditor-decryptable supply delta; trcCiphertext is the Guardian-Committee-decryptable compliance ciphertext. Both encrypt the same hidden mint amount under their respective keys. |
EncryptedBurn(CompressedPoint indexed from, ElGamalCiphertext tsCiphertext, ElGamalCiphertext trcCiphertext) | Emitted by encryptedBurn. Same dual-ciphertext shape as EncryptedMint. |
MinterUpdated, PublicBurnAuthUpdated, EncryptedBurnAuthUpdated | Admin role changes |
The auditor reconstructs the running supply by replaying these four events: PublicMint/PublicBurn carry the plaintext amount, while EncryptedMint/EncryptedBurn carry tsCiphertext they decrypt with totalSupplySK. The on-chain encryptedTotalSupply storage value is also readable directly at any block as a checkpoint.
Security model
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 Safe multisig as the sole 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, target) pairs the owner has explicitly enrolled, 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. Authorizations do not spill: granting (burner, A) does not confer authority over B, regardless of whether the same address controls both. For the encrypted path the burner additionally needs the source ESK out-of-band to satisfy the in-circuit decryption check, so a stolen burner key alone is not enough — but the owner is responsible for never enrolling a (burner, EPK) pair where the burner cannot legitimately obtain the ESK.