ERC-7160: ERC-721 Multi-Metadata Extension
ERC-7160 lets a single NFT expose multiple metadata URIs, with one "active" URI returned by the canonical tokenURI(tokenId) call. Used by:
- Evolving NFTs — character art that changes with story progression; the contract tracks the chapter index, marketplaces show the active one.
- Dynamic-state NFTs — game items with multiple visual states (default / equipped / damaged); state changes flip the active index.
- Multi-vendor NFTs — collections that ship with both a canonical artwork URI and licensed remix URIs from different artists.
- Versioned metadata — V1 / V2 / V3 metadata snapshots, with the contract tracking which is current.
The key advantage over "just call setTokenURI": ERC-7160 keeps all historical URIs available at distinct indices, so off-chain consumers can show "this NFT used to look like X, now looks like Y" or roll back to a previous version.
Required Interface
interface IERC7160 {
event TokenUriPinned(uint256 indexed tokenId, uint256 indexed index);
event TokenUriUnpinned(uint256 indexed tokenId);
/// Returns (URI list, active index, isPinned).
function tokenURIs(uint256 tokenId)
external view returns (uint256 index, string[] memory uris, bool pinned);
function pinTokenURI(uint256 tokenId, uint256 index) external;
function unpinTokenURI(uint256 tokenId) external;
function hasPinnedTokenURI(uint256 tokenId) external view returns (bool);
}The pin / unpin distinction matters: an unpinned NFT defaults to a contract-defined active URI (often the last one added); a pinned NFT sticks to a specific index until explicitly unpinned.
Neo Equivalent: NEP-11 + URI List + Active-Index Storage
NEP-11's properties(tokenId) → Map returns the metadata; ERC-7160 adds a URI list and a per-token active index. The Neo port stores both as prefixed keys:
public static (BigInteger, ByteString[], bool) TokenUris(ByteString tokenId)
{
var uris = GetUris(tokenId);
var index = GetActiveIndex(tokenId);
var pinned = IsPinned(tokenId);
return (index, uris, pinned);
}
public static void PinTokenUri(ByteString tokenId, BigInteger index)
{
if (!Runtime.CheckWitness(OwnerOf(tokenId))) throw new Exception("NEP11:NoAuth");
var uris = GetUris(tokenId);
if (index >= uris.Length) throw new Exception("NEP11:IndexOutOfRange");
SetActiveIndex(tokenId, index);
SetPinned(tokenId, true);
OnTokenUriPinned(tokenId, index);
}| ERC-7160 (Ethereum) | Neo Equivalent | Notes |
|---|---|---|
tokenURIs(tokenId) returning (index, uris[], pinned) | TokenUris(tokenId) returning the same triple | Direct shape |
pinTokenURI(tokenId, index) | PinTokenUri(tokenId, index) with witness check | Owner-only |
unpinTokenURI(tokenId) | UnpinTokenUri(tokenId) resets to contract-default | |
tokenURI(tokenId) returns active URI | NEP-11 properties(tokenId)["uri"] returns active URI | Maintains NEP-11 compatibility |
TokenUriPinned / Unpinned events | OnTokenUriPinned / OnTokenUriUnpinned notifications |
Composition
- ERC-4906 — NFT metadata update event. ERC-7160's pin/unpin should emit ERC-4906's
MetadataUpdate(tokenId)event so marketplaces refresh their cached metadata. - ERC-2981 — royalties. Active URI changes don't affect royalty calculations.
- ERC-5114 + ERC-5192 — soulbound NFTs that evolve. Pin index might be governed by external logic (achievement contract, chapter-progression contract).
Migration Notes
Porting an ERC-7160 collection to Neo:
- Add a per-token URI list storage prefix.
- Add a per-token active-index storage prefix (defaults to 0 if absent).
- Add a per-token pinned-flag storage prefix.
- Override NEP-11's
properties(tokenId)["uri"]to return the active URI. - Implement
PinTokenUri/UnpinTokenUri/TokenUrisper the shape above. - Emit ERC-4906's metadata-update event on every pin/unpin change.
The on-chain cost is per-URI storage; for collections with many URIs per token, consider storing only the IPFS / HTTPS path and computing the full URI from a contract-level base.
