Skip to content
Merged
125 changes: 55 additions & 70 deletions ERCS/erc-8048.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,43 +139,43 @@ For example, an agent's additional account at index `0` on Ethereum mainnet woul

#### Marketplace compatibility

`tokenURI(uint256)` remains the ERC-721 marketplace metadata mechanism and is unchanged by this profile. Contracts SHOULD keep `tokenURI` JSON compatible with existing marketplaces (`name`, `description`, `image`). Agent-aware clients SHOULD read the keys above from this ERC's records rather than from `tokenURI`, and SHOULD treat both endpoints and `context` as untrusted input. Transferring the token transfers ownership of the record, not any offchain server, key, balance, or memory the records may reference. Where the **Metadata Authority** extension is in use, clients SHOULD read `metadataAuthority(tokenId)` to determine whether the contract's own write policy or a distinct metadata authority holds write authority.
`tokenURI(uint256)` remains the ERC-721 marketplace metadata mechanism and is unchanged by this profile. Contracts SHOULD keep `tokenURI` JSON compatible with existing marketplaces (`name`, `description`, `image`). Agent-aware clients SHOULD read the keys above from this ERC's records rather than from `tokenURI`, and SHOULD treat both endpoints and `context` as untrusted input. Transferring the token transfers ownership of the record, not any offchain server, key, balance, or memory the records may reference. Where the **Metadata Authority** extension is in use, clients SHOULD read `tokenMetadataAuthority()` to determine whether the contract's own write policy or a distinct metadata authority holds write authority.

### Optional Metadata Authority Extension

This optional, standalone extension lets a token's metadata be written by a **metadata authority** instead of the token's owner or holders. The authority can be an account or a contract. A contract authority can apply its own rules, for example letting different parties write different keys. It is intended for assets where a party other than the holder governs certain records, for example a registered authority that maintains regulated fields on a real-world asset.
This optional, standalone extension lets the metadata of every token in the collection be written by one **metadata authority** instead of the token's owner or holders. The authority can be an account or a contract. A contract authority can apply its own rules, for example letting different parties write different keys. It is intended for assets where a party other than the holder governs certain records, for example a registered authority that maintains regulated fields on a real-world asset.

An implementation of this extension MUST also implement `IERC8048Metadata` (the core interface above) and MUST implement [ERC-165](./eip-165.md).

The standardized surface is a single read function and one event.

```solidity
interface IERC8048MetadataAuthority {
/// @notice The current metadata authority for `tokenId`.
function metadataAuthority(uint256 tokenId) external view returns (address);
interface IERC8048TokenMetadataAuthority {
/// @notice The current metadata authority for every token in the collection.
function tokenMetadataAuthority() external view returns (address);

/// @notice Emitted whenever `tokenId`'s metadata authority is set, changed, or cleared.
/// @notice Emitted whenever the collection metadata authority is set, changed, or cleared.
/// `authority` is the new authority, or the zero address when cleared.
event MetadataAuthoritySet(uint256 indexed tokenId, address indexed authority);
event TokenMetadataAuthoritySet(address indexed authority);
}
```

#### Extension Interface Detection

This extension's [ERC-165](./eip-165.md) interface ID is `0xf9cb127d`; contracts implementing it MUST return `true` from `supportsInterface` for `0xf9cb127d`.
This extension's [ERC-165](./eip-165.md) interface ID is `0x04f90b41`. Contracts implementing it MUST return `true` from `supportsInterface` for `0x04f90b41`.

#### Authorization

For every function that writes metadata for a token, implementations MUST apply the following authorization semantics:

- When `metadataAuthority(tokenId) != address(0)`, `setMetadata` MUST authorize only that exact authority.
- When `metadataAuthority(tokenId) == address(0)`, no authority is set. Whether anyone can update the token's metadata, and who, is up to the contract's own write policy, and the metadata may not be updatable at all.
- When `tokenMetadataAuthority() != address(0)`, `setMetadata` MUST authorize only that exact authority, for every token, with no exception.
- When `tokenMetadataAuthority() == address(0)`, no authority is set. Whether anyone can update a token's metadata, and who, is up to the contract's own write policy, which may be the owner, the holders or nobody.

A successful metadata write emits `MetadataSet`. A call that fails authorization MUST NOT emit `MetadataSet` and MUST revert.

#### Setting the authority (implementation-specific)

Setting, changing, or clearing the authority is NOT part of this extension's interface and is implementation-defined. Whatever mechanism an implementation uses, it MUST emit `MetadataAuthoritySet` whenever a token's metadata authority is set, changed, or cleared, with `authority` set to the new authority, or to `address(0)` when the authority is cleared.
Setting, changing, or clearing the authority is NOT part of this extension's interface and is implementation-defined. Whatever mechanism an implementation uses, it MUST emit `TokenMetadataAuthoritySet` whenever the address that `tokenMetadataAuthority()` returns changes, with `authority` set to the new authority, or to `address(0)` when the authority is cleared. This includes a change that comes from something else, such as an ownership transfer when the authority follows the contract owner. A contract whose authority starts nonzero MUST emit the event when it is deployed.

#### Example: owner and authority fields on a real-world asset

Expand All @@ -185,7 +185,7 @@ Consider a house tokenized as an [ERC-721](./eip-721.md). Two authorities each m
- The county assessor writes `gov.county-assessor.value` (the assessed value).
- The owner writes `usr.<key>` records (for example `usr.listing-note`).

Using its implementation-specific mechanism, the implementation sets a policy contract as the token's metadata authority. While `metadataAuthority(tokenId)` returns the policy contract, every metadata write goes through it, and owner status alone is not enough. The policy contract lets the housing authority write only the size, the county assessor write only the value, and the owner write `usr.*` keys.
Using its implementation-specific mechanism, the implementation sets a policy contract as the collection's metadata authority. While `tokenMetadataAuthority()` returns the policy contract, every metadata write goes through it, and owner status alone is not enough. The policy contract lets the housing authority write only the size, the county assessor write only the value, and the owner write `usr.*` keys.

### Onchain Metadata Contract Reference (Optional Extension)

Expand Down Expand Up @@ -233,7 +233,7 @@ Security: clients SHOULD only call metadata contracts they consider trustworthy;

## 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. The **Metadata Authority** extension standardizes a read for discovering whether write authority for a token rests with a distinct authority or with the contract's own write policy. One authority per token keeps the interface small. When different parties need to govern different keys, the authority can be a contract that applies those rules. **Onchain Metadata Contract Reference** extends this model to already-deployed tokens by pointing `metadata_contract` in `tokenURI` / `uri` JSON to a sidecar by its contract address.
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. The **Metadata Authority** extension standardizes a read for discovering whether write authority for the collection rests with a distinct authority or with the contract's own write policy. One authority for the whole collection keeps the interface small, because a per-token authority needs one event per token when a collection changes its authority. When different parties, including holders, need to write different keys, they write through a policy contract set as the authority. **Onchain Metadata Contract Reference** extends this model to already-deployed tokens by pointing `metadata_contract` in `tokenURI` / `uri` JSON to a sidecar by its contract address.
## Backwards Compatibility

- Fully compatible with [ERC-721](./eip-721.md), [ERC-1155](./eip-1155.md), and [ERC-6909](./eip-6909.md).
Expand Down Expand Up @@ -285,98 +285,81 @@ contract OnchainMetadataExample is IERC8048Metadata, IERC165 {
pragma solidity ^0.8.20;

import "./IERC8048Metadata.sol";
import "./IERC8048MetadataAuthority.sol";
import "./IERC8048TokenMetadataAuthority.sol";

interface IERC165 {
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

/// @dev Reference implementation of the core interface plus the metadata authority
/// extension, written for ERC-721. It is `abstract`: a concrete contract supplies `_uniqueOwnerOf`
/// (the token's unique current owner, or `address(0)` if the token does not
/// exist). `setMetadataAuthority` is one example assignment policy (the
/// RECOMMENDED one) and is NOT part of the standardized interface; the
/// `MetadataAuthoritySet` it emits is the standardized interface event,
/// inherited from `IERC8048MetadataAuthority`.
abstract contract OnchainMetadataAuthorityExample is
/// extension. By default the collection owner is the metadata authority, so the
/// authority changes when ownership changes. The owner can hand the authority to
/// another address, for example a policy contract. `setTokenMetadataAuthority` is
/// one example of how the authority can be set and is not part of the standard.
/// The `TokenMetadataAuthoritySet` event it emits is.
contract OnchainMetadataAuthorityExample is
IERC8048Metadata,
IERC8048MetadataAuthority,
IERC8048TokenMetadataAuthority,
IERC165
{
// Mapping from tokenId => key => value
mapping(uint256 => mapping(string => bytes)) private _metadata;

// Mapping from tokenId => authority
mapping(uint256 => address) private _metadataAuthority;
address private _owner;

/// @dev The unique current owner of `tokenId`, or `address(0)` if it does not exist.
/// Concrete contracts wire this to their token logic (e.g. ERC-721 `ownerOf`,
/// returning zero instead of reverting for a nonexistent token). ERC-721
/// approvals and operators MUST NOT be returned here.
function _uniqueOwnerOf(uint256 tokenId) internal view virtual returns (address);
// The authority set in place of the owner, or zero while the authority follows the owner
address private _setAuthority;

/// @dev Conceptual ERC-721 approval views the concrete contract wires to its token
/// logic: the single approved address for `tokenId` (`getApproved`) and operator
/// approval for `owner` (`isApprovedForAll`). Used only in the zero-authority
/// metadata-write branch below; they never authorize setting the authority.
function _getApproved(uint256 tokenId) internal view virtual returns (address);
function _isApprovedForAll(address owner, address operator) internal view virtual returns (bool);
constructor(address initialOwner) {
_owner = initialOwner;
emit TokenMetadataAuthoritySet(initialOwner);
}

// --- Reads (unchanged by the extension) ---
function owner() external view returns (address) {
return _owner;
}

function metadata(uint256 tokenId, string calldata key)
external view override returns (bytes memory) {
return _metadata[tokenId][key];
}

/// @notice Return the current authority, or zero when none is set.
function metadataAuthority(uint256 tokenId) external view override returns (address) {
return _metadataAuthority[tokenId];
/// @notice Return the set authority, or the owner while none is set.
function tokenMetadataAuthority() public view override returns (address) {
return _setAuthority == address(0) ? _owner : _setAuthority;
}

// --- Writes (metadata records: owner or ERC-721 approval when no authority; else authority) ---

/// @notice Only the metadata authority can write, for every token.
function setMetadata(uint256 tokenId, string calldata key, bytes calldata value)
external {
address authority = _metadataAuthority[tokenId];
if (authority == address(0)) {
address owner = _uniqueOwnerOf(tokenId);
require(owner != address(0), "nonexistent token");
require(
msg.sender == owner
|| msg.sender == _getApproved(tokenId)
|| _isApprovedForAll(owner, msg.sender),
"not authorized"
);
} else {
require(msg.sender == authority, "not authority");
}
require(msg.sender == tokenMetadataAuthority(), "not authority");
_metadata[tokenId][key] = value;
emit MetadataSet(tokenId, key, key, value);
}

/// @dev Example assignment policy only; not part of IERC8048MetadataAuthority.
/// Demonstrates the RECOMMENDED policy: the owner may set the authority
/// while it is unset; once set, only the current authority may change or
/// clear it (the zero address clears it). Other implementations may differ.
function setMetadataAuthority(uint256 tokenId, address newAuthority) external {
address previous = _metadataAuthority[tokenId];
if (previous == address(0)) {
address owner = _uniqueOwnerOf(tokenId);
require(owner != address(0) && msg.sender == owner, "not owner");
} else {
require(msg.sender == previous, "not authority");
}
_metadataAuthority[tokenId] = newAuthority;
emit MetadataAuthoritySet(tokenId, newAuthority);
/// @dev Example only, not part of the standard. The current authority sets another
/// authority. The zero address returns to following the owner.
function setTokenMetadataAuthority(address newAuthority) external {
require(msg.sender == tokenMetadataAuthority(), "not authority");
_setAuthority = newAuthority;
emit TokenMetadataAuthoritySet(tokenMetadataAuthority());
}

// --- Introspection ---
/// @dev A contract that inherits an ownership module overrides its transfer function
/// to do the same. The event is emitted only while the authority follows the
/// owner, because otherwise the authority does not change.
function transferOwnership(address newOwner) external {
require(msg.sender == _owner, "not owner");
_owner = newOwner;
if (_setAuthority == address(0)) {
emit TokenMetadataAuthoritySet(newOwner);
}
}

/// @notice ERC-165 interface detection: core, authority extension, and ERC-165.
function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
return interfaceId == 0xdf670be1
|| interfaceId == 0xf9cb127d
|| interfaceId == 0x04f90b41
|| interfaceId == type(IERC165).interfaceId;
}
}
Expand All @@ -386,6 +369,8 @@ abstract contract OnchainMetadataAuthorityExample is

This ERC is designed to put metadata onchain, providing security benefits through onchain storage.

A contract set as the metadata authority can be replaced unless the implementation prevents it. The rules of a policy contract hold only while it stays the authority.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
Loading