On-Chain Token Metadata Emission (`TokenMetadata` Events)
| MIP | 0018 |
|---|---|
| MPS | Off-Chain Token Metadata Registry for Midnight (unnumbered; https://github.com/midnightntwrk/midnight-improvement-proposals/pull/104) |
| Status | Proposed |
| Category | Standards |
| Authors | Edward Alvarado |
| Created | 2026-09-17 |
| Requires | MIP-0002: Public Contract Log Emission for Compact |
Abstract
Midnight wallets and explorers cannot tell what a token is. A contract-issued UTXO carries a 32-byte color that identifies its token but no name, and a contract token is whatever its contract implements, with no generic way to find or label it.
This MIP lets a contract publish metadata for its own tokens. It defines two parts:
- A transport layer. Metadata travels in MIP-0002
Miscevents namedmip-0018:token-metadata[v1]. Each event names one token and carries one or more typed key/value records. Each event is bound to the contract that emitted it, so a contract can describe only its own tokens. A user who has the color of a contract-issued UTXO, or a contract token’s contract address, can use it to query that token’s authoritative events. Consumers keep the latest value for each key; a Null record deletes its key, and a token with no keys left is not referenced at all. - Common metadata. Three common keys (
name,symbol,decimals) and the optional keysstandards,imageandimage_sha256give wallets and explorers a shared core for displaying tokens.
The transport is designed as a building block for future standards: later MIPs can define further keys on it without changing the transport, and consumers serve those keys only for tokens that declare the defining standard. Events are emitted when a token is created and for extraordinary updates, never as part of normal token operation.
Motivation
The problem
Every contract-issued UTXO has a color, tokenType(domainSep, contractAddress), that identifies its token.
Mint effects are public in the transaction transcript, so anyone can list every color and the contract that minted it, but nothing says what that token is called or how to display it.
Contract tokens are not visible even at that level. They have no mint effect, and each contract represents them its own way: public balances, encrypted balances, coins or commitments held by users, a mix of these, or anything else a contract can implement. A scanner that does not know the contract cannot tell that a token exists.
MIP-0004, MIP-0011 and MIP-0014 define name(), symbol() and decimals() circuits, but reading metadata through them does not work for generic clients:
- It requires executing contract code against the ledger.
- There is no direct way for an indexer to learn that a value has changed.
- As of October 2026, there is no way to obtain a contract’s executable interface.
With events, a client needs only network calls to an indexer. This MIP does not replace circuit interfaces: it is expected to be one of the building blocks, alongside standards built on circuit interfaces such as these getters.
The consequences are those the Token Registry MPS lists: wallets show 64-character hex strings, explorers cannot label tokens, and DApps hard-code token lists.
Why an off-chain registry is not enough on its own
The Token Registry MPS proposes a CIP-26-style off-chain registry. That is the right place for curation, but it cannot be the only layer:
- Provenance. A registry entry claims to speak for an issuer, but nothing on chain identifies one. A
ContractDeployrecords no creator; the maintenance authority controls upgrades rather than naming the deployer, and may be empty; whoever paid the DUST fee proves nothing. Only the contract itself, by executing, can speak for the contract. An emitted event is exactly that. - Registration. Every issuer must find and submit to the registry. Test tokens, community tokens and LP shares never will.
- Discovery. A registry maps the colors someone registered; the event stream contains every declaration.
Why events
- Self-describing. The event carries the key and the value in a layout fixed by this MIP, so decoding needs no knowledge of the contract.
- Bound to the emitter. Each event is bound to the contract that emitted it; its address comes from the event record, not from the payload.
- Append-only. Renames and corrections are new events, applied in chain order.
- No state growth. Events are not consensus state (MIP-0002, “Event Lifetime”).
- Available now.
emit→Logopcode →VersionedLogItem→ indexerMiscContractEventis shipping; this MIP is only a convention onMisc.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
Terminology
- Color: the 32-byte token type carried by every contract-issued UTXO,
tokenType(domainSep, contractAddress)from the Compact standard library. All UTXOs of one contract-issued UTXO token have the same color. domainSep: 32 bytes identifying a token within its contract, like an ERC-1155id. For a contract-issued UTXO token it is the value passed tomintShieldedTokenormintUnshieldedToken. For a contract token it is a custom identifier the contract chooses for that token.- Contract-issued UTXO token: value held as protocol-level UTXOs (shielded Zswap coins or unshielded UTXOs) and minted by a contract. Its UTXOs carry a color. Other MIPs call these native tokens, such as the native shielded and native unshielded tokens of MIP-0011 and MIP-0014.
- Contract token: a token represented by contract logic rather than protocol UTXOs. The representation is up to the contract (public balances as in MIP-0004, encrypted balances, user-held coins or commitments, or a mix), and this MIP does not depend on it. It uses no color and has no mint effect.
- Token identity:
(network, contractAddress, domainSep, kind);kindis defined in Token identity and authority. - Record: one typed key/value entry in an event.
- Character: a visible character, that is, a grapheme cluster as defined by Unicode Standard Annex #29.
- Field: a token identity plus a key. A field has at most one current value. A token identity exists only while at least one of its fields has a value.
- Tombstone: a record of type Null. It deletes its field.
- Consumer: any reader of these events, such as an indexer, wallet or explorer.
How token identity, field and record relate:
|<-------------- token identity -------------->|
|<---------------------- field ----------------------->|
|<--- record ---->|
| network | contractAddress | domainSep | kind | key | value |
network is the network the event was read from, contractAddress comes from the event record, domainSep and kind come from the payload header, and key and value come from a record in the payload.
Event
A TokenMetadata event is a MIP-0002 Misc event:
Misc {
name: pad(32, "mip-0018:token-metadata[v1]")
// 0x6d69702d303031383a746f6b656e2d6d657461646174615b76315d, then 5 zero bytes
payload: Bytes<256> // see Payload
}
Consumers MUST ignore Misc events with any other name, including other versions of this one.
An event with this name whose payload cannot be decoded as defined in this MIP is invalid: consumers MUST NOT consider it, and none of its records are applied.
Versioning. The name carries the version.
New keys and new standards identifiers need no new version.
A new value type or kind, or a change to the payload format, the validation rules or the common-field meanings, requires a new name such as mip-0018:token-metadata[v2].
Payload
Each payload is 256 bytes and describes one token identity. It has three parts, in order: a header, one or more records, and zero padding.
| domainSep (32 bytes) | kind (1 byte) | record 1 | record 2 | ... | zero padding |
0 32 33 256
Header (the first 33 bytes):
| Offset | Size (bytes) | Field | Meaning |
|---|---|---|---|
| 0 | 32 | domainSep |
Which token within the contract. |
| 32 | 1 | kind |
How the token is represented: 1, 2 or 3; see Token identity and authority. |
Records start at offset 33 and follow each other with no gaps. Each record has five fields:
| Field | Size (bytes) | Meaning |
|---|---|---|
keyLen |
1 | Length of key, 1–255. |
key |
keyLen |
The key, for example name. |
valType |
1 | How to read value; see Value types. |
valLen |
1 | Length of value, 0–255. |
value |
valLen |
The value. |
For example, name = "Acme Token" is the 17-byte record 04 6e616d65 01 0a 41636d6520546f6b656e: keyLen 4, key “name”, valType 1 (UTF-8 string), valLen 10, value “Acme Token”. Appendix A shows a complete payload.
Padding: every byte after the last record, up to byte 256, is zero. A zero keyLen therefore marks the end of the records, which is why a key cannot be empty.
- Every record must fit within the 256 bytes; see Limitations.
- Keys and values are exact byte strings. Lengths count bytes, not characters, and zero bytes inside a key or value are significant.
A consumer MUST check a payload as follows. If any check fails, it MUST reject the whole event and apply none of its records. Rejecting one event never affects another.
kind(byte 32) is 1, 2 or 3.- Starting at byte 33, read records one after another until reaching either byte 256 or a zero
keyLen:- If
keyLenis zero, the records have ended, and every remaining byte up to byte 256 is zero. - Otherwise the key,
valType,valLenand the value all fit within the 256 bytes, and the value follows the rule for itsvalType(see Value types). The next record starts right after the value.
- If
- At least one record was read.
Every emitter MUST produce exactly these bytes, whatever its toolchain.
Compact has no runtime-sized byte strings, so a Compact emitter builds each payload from fixed-size fields (for example Uint<8> lengths and Bytes<K> keys), using the serialization the Compact compiler provides for those types.
Each fixed-size field must be exactly as long as the key or value it carries: a shorter value padded with zero bytes is a different value (for example "AGL" sent as a Bytes<4> is "AGL\0"), and consumers accept it as such.
Value types
valType |
Type | Rule |
|---|---|---|
| 0 | bytes | Any length. |
| 1 | UTF-8 string | Valid UTF-8; may be empty. |
| 2 | unsigned integer | valLen 1–31; little-endian, the Compact serialization of Uint<8 × valLen>. |
| 3 | JSON | One complete UTF-8 JSON value (RFC 8259). |
| 4 | URI | A URI as defined in RFC 3986: a scheme is required, a fragment is allowed, and relative references are not. The value is ASCII only: anything outside ASCII MUST be percent-encoded, and host names converted to their ASCII form, before emitting. |
| 5 | Null | valLen = 0. A tombstone; see Applying records. |
| 6–255 | reserved | Reject the event. |
A value that breaks its type’s rule rejects the event.
Consumers MUST decode integers of every permitted width and MUST NOT reinterpret a value as a different type.
Empty strings, empty bytes, zero bytes and JSON null are ordinary values, not tombstones.
Token identity and authority
kind says how the token is represented:
kind |
Representation | Example | Color |
|---|---|---|---|
| 1 | shielded UTXO | MIP-0011 | yes |
| 2 | unshielded UTXO | MIP-0014 | yes |
| 3 | contract token | MIP-0004 | no |
Any other value rejects the event.
Every record in an event belongs to the token identity (network, contractAddress, domainSep, kind):
networkis the network the event was read from.contractAddressis the address of the contract the event is bound to. Consumers MUST take it from the event record, never from the payload, so a contract can describe only its own tokens.- Each kind is a separate identity with its own metadata events, even when several represent the same asset. A contract may mint one asset both shielded and unshielded and also represent it as a contract token (as MIP-0004 does), and a user may hold all three at once. The contract describes each representation separately; indexers and UIs combine them, for example through Symbol grouping.
- A contract may mint many contract-issued UTXO tokens and keep many contract tokens;
domainSeptells them apart. For a contract token it is any stable 32 bytes the contract chooses, such aspad(32, "acme:gold").
Lookup. A user finds a token’s metadata from what the user holds: the color of a contract-issued UTXO (kinds 1 and 2) or a contract token’s contract address (kind 3).
For contract-issued UTXO tokens, indexers record the contract address and domainSep of every mint in each block, compute color = tokenType(domainSep, contractAddress), and keep a table from color to (contractAddress, domainSep).
A mint is a shieldedMints or unshieldedMints effect of a contract call.
A color held by a user resolves through that table to a token identity, whose metadata events the user then queries.
A shielded and an unshielded mint with the same domainSep have the same color; the kind is given by what the user holds (a shielded coin → kind 1, an unshielded UTXO → kind 2), not by the color.
A color MUST always be computed this way, never read from a value.
For kind 3, querying the contract’s kind-3 events lists its contract tokens. This MIP does not define how a client picks the user’s token among several; that is left to the standards the token declares in standards.
Keys
- Keys are compared as exact bytes: case-sensitive, with no trimming or Unicode normalization.
- A key has meaning only when a MIP defines it, together with its value type. The keys this MIP defines are in Common fields; later MIPs can define others.
- Consumers MUST NOT serve a key that is neither defined in Common fields nor defined by a standard the token currently declares in
standards. What they store is up to them. An event that carries such a key is still valid. - Keys MUST be valid UTF-8.
Applying records
Consumers apply records from accepted events in chain order: block, transaction within the block, event within the transaction, then record within the event. Within a transaction, events are in the ledger’s execution order: the guaranteed part of every intent (in ascending segment id), then each successful fallible segment (in ascending segment id); within a part, actions and their operations in order. For every token it tracks, a consumer’s state MUST equal the result of applying, in that order, every accepted event on the canonical chain. A consumer that follows non-final blocks MUST therefore recompute state when a reorganization removes blocks; alternatively it can follow only finalized blocks.
- Non-Null record: sets that field’s current value, replacing any earlier value. An event changes only the keys it carries; it is not a snapshot. A replaced value MUST NOT be presented as current or used as a fallback; consumers MAY keep it as clearly marked history.
- Null record (tombstone): deletes its field. Consumers MUST stop serving the field’s value and its earlier values, and MUST NOT fall back to an earlier value. A Null record for a field that has no value has no effect. A later non-Null record sets the field again; nothing from before the tombstone returns.
A token identity exists only while at least one of its fields has a value. Once its last field is deleted, consumers MUST NOT reference the identity at all (in listings, lookups, groups or metadata history), as if it had never been described. A later non-Null record describes it again, with only that field. To withdraw a token, emit a Null record for each of its keys; one event can carry them all.
Common fields
For each token identity it describes, a contract SHOULD publish the three common keys name, symbol and decimals, and MAY publish the optional keys standards, image and image_sha256.
| Key | Value type | Requirement | Meaning |
|---|---|---|---|
name |
UTF-8 string (1), 1–64 characters | SHOULD | Display name. |
symbol |
UTF-8 string (1), 1–12 characters | SHOULD | Ticker. |
decimals |
unsigned integer (2) | SHOULD | Number of decimal places: 10^decimals base units make one whole token, so a raw amount is shown as amount / 10^decimals. Emitters SHOULD use Uint<8>, the type MIP-0011 and MIP-0014 use. |
standards |
UTF-8 string (1) | MAY | Standards the token claims to implement; see below. |
image |
URI (4) | MAY | Image or logo of the token. |
image_sha256 |
bytes (0), exactly 32 | MAY | SHA-256 of the file at image. |
- A token that also exposes standard getters (for example those of MIP-0004, MIP-0011 or MIP-0014) SHOULD emit the same values those getters return when it first publishes them. A later update, such as a rename, is the token’s current metadata for consumers of this MIP even where the getters cannot change. A MIP that defines a token standard MAY restrict or override the behavior of this MIP for tokens that declare that standard in the emitted
standardsfield. - A field whose current value lacks the type or form above is unusable: consumers show no value for it and MUST NOT fall back to an earlier value. The event that set it remains valid. A
nameorsymbolover its maximum length is still usable; it is truncated as described next. - Emitters SHOULD keep
nameandsymbolwithin their lengths; consumers SHOULD discard any extra characters. - If
name,symbolordecimalswas never set, there is no value; a token whosedecimalswas never set has unknown decimals. Consumers MUST NOT assume a default, such as 0 or 18 decimals. A MIP that defines a token standard MAY define a default for tokens that declare it. - When
image_sha256is set, consumers MUST NOT display content fromimagewhose SHA-256 differs from it. Withoutimage_sha256, the content cannot be checked; see Off-chain content. standardsis a list of identifiers separated by single spaces (0x20). An identifier is non-empty and contains no byte in0x00–0x20or0x7f(ASCII space and control codes). Identifiers are compared exactly and are case-sensitive; order and duplicates carry no meaning. An empty value, or nostandardsfield, means no standards are claimed; a malformed value is unusable, not empty.- This MIP defines only the list format, not what an identifier means or what claiming it implies. A MIP is identified as
mip-NNNN(for examplemip-0011), and that MIP defines its meaning. A token may also claim standards from elsewhere, such as BIPs or ERCs; their identifiers and what they mean on Midnight SHOULD be defined in a MIP. standardsis self-declared. A consumer MAY use an identifier it recognizes to choose a UI or adapter it already trusts, but MUST NOT treat it as proof of conformance and MUST NOT fetch or run code because of it.
Symbol grouping
Indexers SHOULD group token identities that share (network, contractAddress) and have the same usable symbol, compared as exact bytes.
- A group never spans contracts or networks.
- Grouping is presentation only. Each identity keeps its own fields, and an update to one member changes no other member.
- An identity with no usable
symbolis ungrouped. Changing an identity’ssymbolmoves only that identity; deleting itssymbolremoves it from its group. - Representations of one asset typically share a
symboland MAY use different names, such as “Acme Shielded” and “Acme Unshielded”.
Publishing
- No events in normal operation. Normal token operation, such as mints, transfers and burns, MUST NOT emit metadata events. A contract emits them only when a token is created (or first described, for an existing token) and for extraordinary updates, such as a rename.
- A Compact constructor cannot emit. A contract that wants metadata at deployment exposes a circuit, conventionally
publishMetadata(), which the deployer calls right after deployment. - Who may call an emitting circuit is the contract’s choice. Anyone who can call it can rename or withdraw the token, so it SHOULD be access-controlled, publish-once, or removed after use.
- Records for one token identity MAY be emitted in different events, but SHOULD be grouped into as few events as the size limit allows (Limitations).
- The producer is responsible for making sure the transaction that emits the event is included and executed in a block.
- Everything emitted is public.
Consuming
- Reading events. How consumers obtain events is defined by MIP-0002. Some sources drop trailing zero bytes; consumers MUST treat missing trailing bytes as zero, so that every
nameis 32 bytes and everypayload256 bytes, before decoding. - Completeness. Verifying the events a service returned does not prove that no later update or tombstone exists, and a field missing from a filtered response is not proof that it was never set. Indexers MAY index only some tokens or keys.
- Untrusted input. Every payload byte is attacker-controlled. Consumers MUST bounds-check every length before slicing and MUST harden their UTF-8, JSON and URI parsers. Consumers MUST NOT fetch a URI from a value without the precautions they apply to any remote content.
Limitations
- The maximum size of a value is 219 bytes: the 256-byte payload minus the 33-byte header, the 3 bytes of
keyLen,valTypeandvalLen, and a 1-byte key. In general, the largest value is220 − keyLenbytes. - Each event describes one token identity; metadata for another identity needs another event.
Off-chain content
Content that does not fit in an event, such as an image or a document, stays off chain.
If off-chain content is used, the MIP that defines it SHOULD include a way to validate that content, such as a URL together with a hash of the content. image and image_sha256 follow this pattern.
Out of scope
- Schemas beyond the common fields: real-world-asset (RWA) fields, media other than
image, localization, privacy properties and cross-chain mappings. Later MIPs can define them as keys on this transport. - Data that changes during normal operation, such as supply, prices or per-item data. It belongs off chain; see Publishing.
- Curation: deciding which of several tokens with the same name or symbol is genuine. That belongs to registries and allowlists, which can be seeded from these events.
- NIGHT and DUST: their properties are fixed by the protocol.
- Private metadata: every event is public. Selective disclosure belongs to later event work (MPS-0005).
Rationale
Miscrather than a new event type. A dedicatedLogEventTypewould need a ledger release and a coordinated node upgrade, and would freeze the format in the protocol.Miscworks today, the versioned name gives a way to evolve, and the convention can be promoted to a dedicated type later.domainSeprather than color. The color is derivable fromdomainSepand the emitting contract’s address. Sending it would add 32 redundant bytes a contract could forge; deriving it means a contract can never claim another contract’s color.domainSepalso covers contract tokens, which use no color.- A
kindbyte. One asset can exist as shielded and unshielded contract-issued UTXO tokens and as a contract token at once, and each representation needs its own metadata and lifecycle; combining them is left to indexers and UIs. - One contract kind. A contract token’s privacy is a spectrum, not a shielded/unshielded pair: how it is stored, moved and used can each be more or less private, and the contract can change that without creating a different asset. A two-value tag would not describe it, so contract tokens share one kind.
domainSepfor every kind. A contract can mint many contract-issued UTXO tokens and keep many contract tokens, so every kind needs a stable identifier.nameandsymbolcannot serve: they can be renamed or deleted, and other tokens can copy them.- Grouping by symbol within one contract. One contract can represent an asset in several ways (shielded UTXO, unshielded UTXO, contract token, or under several
domainSepvalues), and a wallet should be able to show them together. Limiting a group to one contract stops another contract from joining it by copying the symbol. - Typed values. EIP-7496 stores trait values as untyped
bytes32and describes their types off chain, which helps only consumers that already know the collection. One type byte lets a generic explorer decode a key it has never seen. - Variable-length packed records. The
Miscpayload is fixed at 256 bytes. One-byte lengths suffice within that limit, variable-length keys avoid spending 32 bytes on every key, and packing fits the three common fields plusstandardsin one event (95 bytes in Appendix A). - A finite key set. Consumers serve only keys that this MIP or a declared standard defines, so the transport cannot be used as a general key-value store served as token metadata.
- Latest value per key. As in EIP-7496, an update touches one key and costs one record, with no need to re-emit everything.
- No events in normal operation. Events are not consensus state, but each one still costs fees, block space and storage in every indexer. A token can see thousands of mints and transfers; emitting metadata with them would create thousands of events for values that consumers reduce to one current value per key. The chain is not the place for that data; it belongs off chain, referenced as described in Off-chain content if needed.
- Per-key tombstone. Every record touches exactly one field: a non-Null record sets it and a Null record deletes it. A consumer’s state is therefore just the fields that currently have a value, and a token exists only while it has some.
- Events rather than a metadata field in state. As of October 2026, there is no way to read or prove what a value in a contract’s ledger state means; an event carries the key and value in one fixed layout defined by this MIP. State also grows every node’s storage and keeps no history.
- On-chain names despite MIP-0014. MIP-0014 rejects on-chain
name/symbolas inviting impersonation without removing the color-derivation check. Impersonation is just as easy in an off-chain registry. What this MIP adds is that every claim is bound to the contract that made it, and the derivation check is automatic because a consumer only ever derives colors. Which token to trust remains curation, which is out of scope.
Alternatives considered.
- Circuit code only (reading metadata through
name(),symbol()anddecimals()getters): requires executing contract code against the ledger, gives indexers no direct way to learn that a value changed, and, as of October 2026, a contract’s executable interface cannot be obtained. Kept as a complementary building block, alongside this MIP. - Off-chain registry only: needs attestations and governance to recover the provenance an emitted event has by construction. Kept as the curation layer.
- URI only (ERC-721
tokenURI): every field becomes a remote fetch that adds a liveness dependency, and a plain URL gives no way to check what was fetched. Off-chain content remains possible, with a way to validate it (Off-chain content). - A protocol-level event type: deferred, as above.
- MIP-0019 multipart events for longer values: would let one payload span several events, but every consumer would then have to collect and join events before parsing them. Metadata values are short, and larger content stays off chain (Off-chain content).
Path to Active
Acceptance Criteria
- At least two independent consumers (indexer, explorer or wallet) pass every vector in Testing.
- At least one issuer other than the authors adopts the convention on a public network.
- A reference Compact module, tested against the vectors, is published in this repository or an ecosystem library such as OpenZeppelin Compact Contracts.
- Community review through the MIP process, including agreement with the Token Registry MPS authors on the on-chain/off-chain boundary.
Implementation Plan
- Publish a Compact module and example issuers in midnight-experiments/mip-0018.
- Publish the test vectors as byte fixtures and a reference decoder, and obtain a second independent reader.
- Deploy a publisher on a public test network.
- Propose the module to OpenZeppelin Compact Contracts as an optional extension of
NativeShieldedToken,NativeShieldedTokenFamilyandFungibleToken. - Publish an upgrade template for pre-v9 contracts and exercise it on a public test network.
Backwards Compatibility Assessment
No protocol, compiler or indexer change is required; this MIP uses MIP-0002’s existing Misc event.
It adds to MIP-0004, MIP-0011 and MIP-0014 tokens without changing their circuits.
A contract that never emits is simply undescribed.
Existing contracts
All contracts can emit once MIP-0002 is in effect (ledger v9, Midnight 2.x). A contract deployed before then has no emitting circuit, but it does not need redeployment: its maintenance authority can add one. Note: a valid maintenance authority is required.
- Compile a
publishMetadata()circuit (and optionallysetMetadata(...)) against the contract’s existing ledger layout. It reads the existingdomainfrom state and emits it asdomainSep. - Add its verifier key with a
VerifierKeyInsertmaintenance update signed by the maintenance authority. - Call it.
The contract address, domainSep, color and every existing coin and balance are unchanged.
The event still comes from the token’s own contract, so authority holds, and the update needs no permission beyond what the issuer already reserved.
Security Considerations
- Impersonation. Any contract can emit any
nameorsymbol, including one another token already uses. A record is bound to its contract and color, not to a brand. Consumers MUST keep each claim attached to its token identity and SHOULD make that identity visible to users. They SHOULD require a curation signal (allowlist, registry attestation or user confirmation) before presenting a token as a known asset. This matches EVM chains and unattested CIP-26 entries. - Curation is a separate layer. These events are an issuer-written base layer. Deciding which of several tokens with the same name or symbol is genuine is curation, which this MIP does not attempt. A registry such as the one proposed in the Token Registry MPS (PR #104) can provide it on top of these events.
- Shared symbols. Anyone can copy a symbol. Because a group never spans contracts, copying one does not put a token into another contract’s group. Consumers MUST NOT treat tokens from different contracts as the same asset because their
symbolvalues match, and membership of a group within one contract does not prove its members are interchangeable. - Unauthorized updates. An unguarded emitting circuit lets anyone rename or withdraw a token. Consumers MAY show a field’s earlier values, marked as history, so hostile renames are visible. A tombstone also removes that field’s history from metadata views, although the events remain on chain.
- Self-declared standards. Advertising an identifier in
standardsproves nothing; see Common fields. - Untrusted payloads. See Consuming.
- Off-chain content. A URL alone says nothing about what its host serves, and the host can change it at any time.
image_sha256, or a similar hash for other off-chain content (Off-chain content), lets consumers detect substituted content. Images are untrusted content; consumers SHOULD render them in a way that cannot run scripts, for example not as inline SVG. - Spam and cost. Events pay the existing
Logfee and compete for each block’sbytes_writtenbudget (MIP-0002, “Fee Metering”). Indexers MAY limit what they index. - Disclosure.
emitis a disclosure point checked by the compiler, so this MIP introduces no new leak path.
Implementation
A publisher writes the header, its records and zero padding, and emits the payload under the event name.
A reader filters by event type and name, parses and validates the payload (Payload), applies records in order (Applying records) and evaluates the common fields (Common fields).
Issuers choose their domainSep values, kinds, extra keys and access control.
The reference implementation (libraries, examples and tests) lives in midnight-experiments/mip-0018.
Dependencies
- MIP-0002
Miscevents: Compact 0.34.0 or later (language 0.26.0, runtime 0.19.0 or later), Midnight ledger v9.
Testing
These vectors are normative.
Unless stated otherwise, the header is domainSep = 0x11 repeated 32 times and kind = 3.
Must accept:
- A1. Common fields and
standards.name = "Acme Token",symbol = "ACME",decimals = 6asUint<8>andstandards = "mip-0004". Records start at offsets 33, 50, 63 and 75; content ends at offset 95, followed by 161 zero bytes. The full bytes are in Appendix A. - A2. Capacity. One type-0 (bytes) record with a 220-byte key and an empty value, or with a 1-byte key and a 219-byte value, fills exactly 256 bytes with no padding.
- A3. Exact bytes.
symbolandsymbolfollowed by0x00are different keys. The type-0 value01 00 00keeps its trailing zeros. - A4. Integer widths.
decimalsas06(Uint<8>) and as06followed by 15 zero bytes (Uint<128>) both decode to 6. - A5. Non-tombstones. An empty string (type 1), empty bytes (type 0) and JSON
null(type 3) are ordinary values. - A6. Image.
image = "https://example.org/acme.png"(type 4) and a 32-byteimage_sha256(type 0) in one event.
Must reject the whole event, applying none of its records:
- R1. A header with no records (bytes 33–255 all zero).
- R2. A key, type byte, length byte or value that extends past byte 256, such as a 221-byte key with an empty value, or a 1-byte key with a 220-byte value.
- R3. After a valid record, a zero
keyLenfollowed by any non-zero byte. - R4.
kind0, 4 or 255;valType6 or higher. - R5. Invalid UTF-8 in a key or in a type-1, type-3 or type-4 value; invalid JSON; a relative URI; an integer of 0 or 32 bytes; a Null record with
valLen> 0. - R6. A valid record, including a tombstone, followed by an invalid one.
Must ignore: Misc events with any other name, including mip-0018:token-metadata[v2], and events of other types.
State rules:
- S1. Order within an event. For kind 1, records
name = "A", Null at keynameandname = "B"(at offsets 33, 41 and 48) leave the identity with onlyname = "B". Without the Null,name = "A"followed byname = "B"in one event also leavesname = "B". - S2. Latest value wins.
name = "Alpha",symbol = "ALP",decimals = 2andstandards = "mip-0004"produce the same state whether sent as four events or one. A later event with onlyname = "Beta"changes onlyname;Alphais never shown as current or used as a fallback. - S3. Tombstone. Publish
name,symbol,decimalsandstandardsfor kinds 1 and 3 under onedomainSep, then a Null record at keynamefor kind 1. Kind 1 keepssymbol,decimalsandstandardsand has noname; kind 3 is unchanged. A second Null atname, or a Null at a key with no value (retire), changes nothing. Null records for kind 1’s remaining keys, in one event, leave kind 1 with no fields, so it is not referenced anywhere; kind 3 is unchanged. A later kind-1name = "New"describes kind 1 again with onlyname;standardsreads as empty and the earlier list does not return. - S4. Reorganization. Removing the block that deleted kind 1’s last fields in S3 restores those fields; re-adding it deletes them again.
- S5. Unusable fields. After usable values, an empty
name, a type-1decimalsof"6", orstandards = "mip-0004 mip-0011"(two spaces) is accepted, makes that field unusable and does not fall back to the earlier value. - S6. Separate identities. The same records under kinds 1, 2 and 3 create three identities, and the same payload from two contracts creates separate identities. Updating one leaves the others unchanged. No color is computed for kind 3.
- S7. Independent events. Of two events in one transaction, a malformed one is rejected and the other applies normally.
- S8. Display. A consumer that displays amounts shows the raw amount
123456withdecimals = 2as1234.56. - S9. Symbol grouping. Grouping is a SHOULD, so two outcomes are valid: no groups at all, or exactly the following groups. Kinds 1 and 3 of one contract with
symbol = "ACME"form one group, and an identity of the same contract under anotherdomainSepwithsymbol = "ACME"joins it. An identity withsymbol = "ACME"from another contract, or on another network, does not. Neither doacme, ` ACME, a type-0 valueACME, the keySYMBOL, or a missingsymbol. Updating one member'snamechanges no other member; changing one member'ssymbolmoves only that member; a Null record at itssymbol` removes it from the group. - S10. Served keys. A record
foo = "x"(type 1) is accepted but not served. If a standardSdefines keyk, a record forkis served only while the token’s currentstandardslistsS. - S11. Name and symbol length. A 70-character
nameis truncated to its first 64 characters and a 15-charactersymbolto its first 12. - S12. Image hash. With
image_sha256set, content fromimagewhose SHA-256 differs is not displayed. Animage_sha256that is not 32 bytes is unusable.
References
- MIP-0002: Public Contract Log Emission for Compact Smart Contracts
- MIP-0004: Fungible Token Standard with UTXO Conversion
- MIP-0011: Native Shielded Token Standard
- MIP-0014: Native Unshielded Token Standard
- MIP-0019: Multipart Event
- MPS-0005: Event Emission Support for Compact Smart Contracts
- Token Registry MPS: “Off-Chain Token Metadata Registry for Midnight”, unnumbered at the time of writing, open as PR #104
- EIP-7496: NFT Dynamic Traits
- ERC-1155: Multi Token Standard
- ERC-721: Non-Fungible Token Standard (
tokenURI) - RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format
- CIP-26: Cardano Off-Chain Metadata
- Reference implementation: midnight-experiments/mip-0018
- OpenZeppelin Compact Contracts
Acknowledgements
- Robert Blessing-Hartley (@bobblessinghartley), for the Token Registry problem statement this MIP responds to.
- The reviewers on PR #104 (@kapke, @DpacJones, @rongurlavi) for the discussion on the on-chain/off-chain boundary and NFT content metadata.
- Dominik Zajkowski (@dzajkowski) for MIP-0002, without which there is no transport.
- The Wallet Group, for feedback and suggested improvements.
Copyright Waiver
All contributions (code and text) submitted in this MIP must be licensed under the Apache License, Version 2.0. Submission requires agreement to the Midnight Foundation Contributor License Agreement, which includes the assignment of copyright for your contributions to the Foundation.
Appendix A: Example event (informative)
The payload of test A1: the three common fields plus standards, for domainSep = 0x11 × 32, kind 3.
offset bytes (hex) meaning
0 11 × 32 domainSep
32 03 kind = contract token
33 04 6e616d65 01 0a 41636d6520546f6b656e name = "Acme Token"
50 06 73796d626f6c 01 04 41434d45 symbol = "ACME"
63 08 646563696d616c73 02 01 06 decimals = 6 (Uint<8>)
75 09 7374616e6461726473 01 08 6d69702d30303034 standards = "mip-0004"
95 00 × 161 padding
Each record reads keyLen | key | valType | valLen | value.
Appendix B: Token Registry MPS fields (informative)
| Token Registry MPS field | This MIP |
|---|---|
subject (color) |
Derived for kinds 1 and 2 as tokenType(domainSep, contractAddress); never transmitted. |
tokenOrigin (domain, contract) |
domainSep from the payload; contractAddress from the event record. |
name, ticker, decimals |
Common fields name, symbol, decimals. |
logo |
image and image_sha256. |
description, url |
Left to a later schema MIP. |
| quadrant | kind: Ledger shielded → 1, Ledger unshielded → 2, both Contract quadrants → 3. |
privacy |
Contract-issued UTXO tokens: the shielded or unshielded kind. Contract tokens: not encoded, since their privacy is a spectrum; a later MIP may define keys for it. |
contractToken.standard |
standards. |
bridge |
Not defined; an issuer may emit a key for it. |
| attestation, sequence number | Chain execution and chain order. |
| governance / review | Out of scope; the registry’s curation role. |