Onchain Metadata for Token Registries
Abstract
This ERC defines an onchain metadata standard for multi-token and NFT registries including ERC-721, ERC-1155, and ERC-6909. The standard provides a key-value store allowing for arbitrary bytes to be stored onchain.
Motivation
This ERC addresses the need for fully onchain metadata while maintaining compatibility with existing ERC-721, ERC-1155, and ERC-6909 standards. It has been a long-felt need for developers to store metadata onchain for NFTs and other multitoken contracts; however, there has been no uniform standard way to do this. Some projects have used the tokenURI field to store metadata onchain using Data URLs, which introduces gas inefficiencies and has other downstream effects (for example making storage proofs more complex). This standard provides a uniform way to store metadata onchain, and is backwards compatible with existing ERC-721, ERC-1155, and ERC-6909 standards.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Scope
This ERC is an optional extension that MAY be implemented by any ERC-721, ERC-1155, or ERC-6909 compliant registries.
Required Metadata Function and Event
Contracts implementing this ERC MUST implement the following interface:
interface IERC8048Metadata {
/// @notice Get metadata value for a key.
function metadata(uint256 tokenId, string calldata key) external view returns (bytes memory);
/// @notice Emitted when metadata is set for a token.
event MetadataSet(uint256 indexed tokenId, string indexed indexedKey, string key, bytes value);
}
metadata(tokenId, key): Returns the metadata value for the given token ID and key as bytes
Contracts implementing this ERC MAY also expose a setMetadata(uint256 tokenId, string calldata key, bytes calldata value) function to allow metadata updates, with write policy determined by the contract.
Contracts implementing this ERC MUST emit the following event when metadata is set:
event MetadataSet(uint256 indexed tokenId, string indexed indexedKey, string key, bytes value);
Interface ID
The interface ID is 0xdf670be1.
Key/Value Pairs
This ERC specifies that the key is a string type and the value is bytes type. This provides flexibility for storing any type of data while maintaining an intuitive string-based key interface.
Optional Key Parameters
Keys MAY include parameters to represent variations or instances of a metadata type, such as "registration/1" or "name/Maria"; see ERC-8119 for the standard parameterized key format.
Optional Diamond Storage
Contracts implementing this ERC MAY use Diamond Storage pattern for predictable storage locations. If implemented, contracts MUST use the namespace ID "erc8048.onchain.metadata.storage".
The Diamond Storage pattern provides predictable storage locations for data, which is useful for cross-chain applications using inclusion proofs. For more details on Diamond Storage, see ERC-8042.
Examples
It is possible to use this standard to tokenize an agent as an NFT: each token ID is the agent, and metadata holds agent fields as UTF-8 in bytes.
Example: AI agent metadata (NFT as agent)
Two key patterns:
context— one key. UTF-8 Markdown is enough for humans and models; issuers MAY also embed a JSON code block so clients can parse token lists, policy URIs, or version fields without a second key.endpoint[<type>]— one URL per protocol;<type>is lowercase (mcp,a2a,ag-ui, …).
Example for tokenId == 1: store the UTF-8 encoding of a Markdown document like the following as the bytes value for "context" (shown as a file; not a Solidity literal):
I am an agent that can swap tokens on Ethereum mainnet. I maintain an official token list and only suggest swaps where both assets appear on that list.
```json
{
"tokenListUri": "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/tokenlist.json",
"network": "mainnet",
"defaultSlippageBps": 50
}
```
Endpoint keys for the same token:
"endpoint[mcp]"→bytes("https://agents.example.com/mcp/1")"endpoint[a2a]"→bytes("https://agents.example.com/a2a/1")"endpoint[ag-ui]"→bytes("https://agents.example.com/ui/1")
Example: Biometric Identity for Proof of Personhood
A biometric identity system using open source hardware to create universal proof of personhood tokens.
- Key:
"biometric_hash"→ Value:bytes(bytes32(identity_commitment)) - Key:
"verification_time"→ Value:bytes(bytes32(timestamp)) - Key:
"device_proof"→ Value:bytes(bytes32(device_attestation))
Optional Metadata Hooks
Contracts implementing this ERC MAY use metadata hooks to redirect record resolution to a different contract for secure resolution from known contracts, such as singleton registries with verifiable security properties.
For the full specification of metadata hooks, see ERC-8121 (Metadata Hooks). Hooks are encoded in the metadata value itself and allow clients to jump to another contract to resolve the metadata value. When using hooks for token metadata with this ERC, the return type MUST be bytes and the hook encoding MUST be bytes.
Onchain Metadata Contract Reference (Optional Extension)
Many ERC-721, ERC-1155, and ERC-6909 registries are already deployed without the metadata function or without storage reserved for the onchain key-value mapping. Adding that interface or storage layout is often impossible without an upgrade path, so those contracts cannot adopt the core pattern of this ERC on the token contract itself.
This optional extension keeps discovery compatible with existing tokenURI / uri flows while directing clients to a separate metadata contract that implements metadata(uint256,string) as defined in this ERC. The target is identified by an ERC-7930 Interoperable Address carried in JSON. The function to call and its signature are fixed by this specification.
tokenURI / uri JSON field
When tokenURI (ERC-721), uri (ERC-1155), or the equivalent URI function for ERC-6909 returns a string that resolves to a JSON document (including JSON embedded in a data: URL), the document MAY include a top-level string property named "metadata_contract".
The value of "metadata_contract" MUST be the interoperable address encoded as a single JSON string: a 0x prefix followed by the hex encoding of the ERC-7930 bytes, all lowercase.
Example:
{
"name": "Example",
"image": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi",
"metadata_contract": "0x00010000010114d8da6bf26964af9d7eed9e03e53415d37aa96045"
}
Client behavior
Clients that support this extension:
- Obtain and parse the JSON from
tokenURI/urias they already do for display metadata. - If a top-level
"metadata_contract"string is present, decode the value as ERC-7930 interoperable address bytes from the0x-prefixed hex string. - Parse the interoperable address to obtain the target chain and contract address.
- Read
metadata(uint256 tokenId, string key)from that contract on the target chain, using the sametokenIdas for thetokenURI/uricall and the metadata key from this ERC. The return value is the same as if the token contract had stored the mapping locally.
In most cases the metadata contract SHOULD be on the same chain as the NFT token registry, but it is possible to use a metadata contract on a different chain, depending on support for cross-chain reads in clients and in the ecosystem in general.
Clients that do not implement this extension or do not trust the target contract address MAY ignore "metadata_contract" and continue to use other JSON fields.
Relationship to IERC8048Metadata
Registries using only this extension are NOT required to implement IERC8048Metadata on the token contract. The contract referenced by metadata_contract MUST implement the metadata(uint256,string) external view returns (bytes) function from this ERC (or a compatible implementation).
Security: clients SHOULD only call metadata contracts they consider trustworthy; a malicious or mistaken metadata_contract could return attacker-controlled bytes. Clients SHOULD show or verify the target address the same as for any new contract interaction.
Rationale
This ERC standardizes a simple string-key, bytes-value metadata store for existing token registries. The optional setMetadata function allows updates under the contract’s chosen write policy, and MetadataSet provides an onchain audit trail. Onchain Metadata Contract Reference extends this model to already-deployed tokens by pointing metadata_contract to a sidecar via a ERC-7930 address in tokenURI / uri JSON.
Backwards Compatibility
- Fully compatible with ERC-721, ERC-1155, and ERC-6909.
- Non-supporting clients can ignore the scheme.
- The Onchain Metadata Contract Reference extension only adds an optional JSON property; clients that do not read
"metadata_contract"behave as they do today.
Reference Implementation
The interface is defined in the Required Metadata Function and Event section above. Here are reference implementations:
Basic Implementation
pragma solidity ^0.8.25;
import "./IERC8048Metadata.sol";
contract OnchainMetadataExample is IERC8048Metadata {
// Mapping from tokenId => key => value
mapping(uint256 => mapping(string => bytes)) private _metadata;
/// @notice Get metadata value for a key
function metadata(uint256 tokenId, string calldata key)
external view override returns (bytes memory) {
return _metadata[tokenId][key];
}
/// @notice Set metadata for a token (optional implementation)
function setMetadata(uint256 tokenId, string calldata key, bytes calldata value)
external {
_metadata[tokenId][key] = value;
emit MetadataSet(tokenId, key, key, value);
}
}
Diamond Storage Implementation
pragma solidity ^0.8.20;
import "./IERC8048Metadata.sol";
contract OnchainMetadataDiamondExample is IERC8048Metadata {
struct OnchainMetadataStorage {
mapping(uint256 tokenId => mapping(string key => bytes value)) metadata;
}
// keccak256("erc8048.onchain.metadata.storage")
bytes32 private constant ONCHAIN_METADATA_STORAGE_LOCATION =
keccak256("erc8048.onchain.metadata.storage");
function _getOnchainMetadataStorage() private pure returns (OnchainMetadataStorage storage $) {
bytes32 location = ONCHAIN_METADATA_STORAGE_LOCATION;
assembly {
$.slot := location
}
}
function metadata(uint256 tokenId, string calldata key)
external view override returns (bytes memory) {
OnchainMetadataStorage storage $ = _getOnchainMetadataStorage();
return $.metadata[tokenId][key];
}
function setMetadata(uint256 tokenId, string calldata key, bytes calldata value)
external {
OnchainMetadataStorage storage $ = _getOnchainMetadataStorage();
$.metadata[tokenId][key] = value;
emit MetadataSet(tokenId, key, key, value);
}
}
Implementations should follow the standard ERC-721, ERC-1155, or ERC-6909 patterns while adding the required metadata function and event.
Security Considerations
This ERC is designed to put metadata onchain, providing security benefits through onchain storage.
Implementations that choose to use the optional Diamond Storage pattern should consider the security considerations of ERC-8042.
Copyright
Copyright and related rights waived via CC0.