-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add EIP: ERC-721 Multi-Metadata Extension
Merged by EIP-Bot.
- Loading branch information
Showing
1 changed file
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
--- | ||
eip: 7160 | ||
title: ERC-721 Multi-Metadata Extension | ||
description: Multiple metadata URIs per token, with the option to pin a primary URI. | ||
author: 0xG (@0xGh), Marco Peyfuss (@mpeyfuss) | ||
discussions-to: https://ethereum-magicians.org/t/erc721-multi-metadata-extension/14629 | ||
status: Draft | ||
type: Standards Track | ||
category: ERC | ||
created: 2023-06-09 | ||
requires: 165, 721 | ||
--- | ||
|
||
## Abstract | ||
|
||
This EIP proposes an extension to the [ERC-721](./eip-721.md) standard to support multiple metadata URIs per token. It introduces a new interface, `IERC721MultiMetadata`, which provides methods for accessing the metadata URIs associated with a token, including a pinned URI index and a list of all metadata URIs. The extension is designed to be backward compatible with existing `ERC721Metadata` implementations. | ||
|
||
## Motivation | ||
|
||
The current [ERC-721](./eip-721.md) standard allows for a single metadata URI per token with the `ERC721Metadata` implementation. However, there are use cases where multiple metadata URIs are desirable. Some example use cases are listed below: | ||
|
||
- A token represents a collection of (cycling) assets with individual metadata | ||
- An on-chain history of revisions to token metadata | ||
- Appending metadata with different aspect ratios so that it can be displayed properly on all screens | ||
- Dynamic and evolving metadata | ||
- Collaborative and multi-artist tokens | ||
|
||
This extension enables such use cases by introducing the concept of multi-metadata support. | ||
|
||
The primary reason for having a multi-metadata standard in addition to the existing `ERC721Metadata` standard is that dapps and marketplaces don't have a mechanism to infer and display all the token URIs. Giving a standard way for marketplaces to offer collectors a way to pin/unpin one of the metadata choices also enables quick and easy adoption of this functionality. | ||
|
||
## Specification | ||
|
||
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. | ||
|
||
**The multi-metadata extension is OPTIONAL for [ERC-721](./eip-721.md) contracts and it is RECOMMENDED to be used in conjuction with the [ERC-4906](./eip-4906.md) standard if implemented**. | ||
|
||
```solidity | ||
/// @title EIP-721 Multi-Metdata Extension | ||
/// @dev See https://eips.ethereum.org/EIPS/eip-721 | ||
/// Note: the ERC-165 identifier for this interface is 0x06e1bc5b. | ||
interface IERC721MultiMetadata /* is IERC721Metadata */ { | ||
/// @dev This event emits when a token uri is pinned and is | ||
/// useful for indexing purposes. | ||
event TokenUriPinned(uint256 indexed tokenId, uint256 indexed index, address indexed sender); | ||
/// @dev This event emits when a token uri is unpinned and is | ||
/// useful for indexing purposes. | ||
event TokenUriUnpinned(uint256 indexed tokenId, address indexed sender); | ||
/// @notice Get all token uris associated with a particular token | ||
/// @dev If a token uri is pinned, the index returned should be the index in the string array | ||
/// @param tokenId The identifier for the nft | ||
/// @return index An unisgned integer that specifies which uri is pinned for a token (or the default uri if unpinned) | ||
/// @return uris A string array of all uris associated with a token | ||
function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris); | ||
/// @notice Pin a specific token uri for a particular token | ||
/// @param tokenId The identifier of the nft | ||
/// @param index The index in the string array returned from the `tokenURIs` function that should be pinned for the token | ||
function pinTokenURI(uint256 tokenId, uint256 index) external; | ||
/// @notice Unpin metadata for a particular token | ||
/// @dev This should reset the token to the default uri | ||
/// @param tokenId The identifier of the nft | ||
function unpinTokenURI(uint256 tokenId) external; | ||
/// @notice Check on-chain if a token id has a pinned uri or not | ||
/// @dev Useful for on-chain mechanics | ||
/// @param tokenId The identifier of the nft | ||
/// @return pinned A bool specifying if a token has metadata pinned or not | ||
function hasPinnedTokenURI(uint256 tokenId) external view returns (bool pinned); | ||
} | ||
``` | ||
|
||
The `TokenUriPinned` event MUST be emitted when pinning a token uri with the `pinTokenUri` function. | ||
|
||
The `TokenUriUnpinned` event MUST be emitted when unpinning a token uri with the `unpinTokenUri` function. | ||
|
||
The `tokenURI` function defined in the ERC-721 Metadata standard MUST return the pinned URI when a token has a pinned uri. The `tokenURI` fucntion MUST return a default uri when a token has an unpinned uri. Which uri is returned when unpinned is up to the developer and is not specified in this standard. This ensures backwards compatibility with existing contracts and applications that rely on the single metadata URI. | ||
|
||
The `supportsInterface` method MUST return `true` when called with `0x06e1bc5b`. | ||
|
||
Implementing functionality to add uris to a token MUST be implemented separately from this standard. A `MetadataUpdate` or `BatchMetadataUpdate` event SHOULD be emitted when adding a uri to a token. | ||
|
||
See the [Implementation](#reference-implementation) section for an example. | ||
|
||
## Rationale | ||
|
||
The `tokenURIs` function MUST revert if the token does not exist. | ||
|
||
The `tokenURIs` function returns both the pinned URI index and the list of all metadata URIs to provide flexibility in accessing the metadata. | ||
|
||
The pinned URI can be used as a default or primary URI for the token, while the list of metadata URIs can be used to access individual assets' metadata within the token. Marketplaces could present these as a gallery or media carousels. | ||
|
||
Depending on the implementation, the `pinTokenURI` function allows the contract owner or token owner to specify a particular fixed metadata URI index for a token. This enables the selection of a preferred URI by index from the list of available metadata. | ||
|
||
When unpinned, it is recommended to return the last URI for the token. However the behavior in the case of unpinned tokens is at the discretion of the implementation and depends on the specific purpose of the token. | ||
|
||
## Backwards Compatibility | ||
|
||
This extension is designed to be backward compatible with existing [ERC-721](./eip-721.md) contracts. The implementation of the `tokenURI` method must either return the pinned token uri (if pinned) or some default uri (if unpinned). | ||
|
||
## Reference Implementation | ||
|
||
An open-source reference implementation of the `IERC721MultiMetadata` interface can be provided, demonstrating how to extend an existing [ERC-721](./eip-721.md) contract to support multi-metadata functionality. This reference implementation can serve as a guide for developers looking to implement the extension in their own contracts. | ||
|
||
```solidity | ||
// SPDX-License-Identifier: CC0-1.0 | ||
pragma solidity ^0.8.19; | ||
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; | ||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | ||
import {IERC4906} from "@openzeppelin/contracts/interfaces/IERC4906.sol"; | ||
import {IERC721MultiMetadata} from "./IERC721MultiMetadata.sol"; | ||
contract MultiMetadata is ERC721, Ownable, IERC721MultiMetadata, IERC4906 { | ||
mapping(uint256 => string[]) private _tokenURIs; | ||
mapping(uint256 => uint256) private _pinnedURIIndices; | ||
mapping(uint256 => bool) private _hasPinnedTokenURI; | ||
constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) Ownable() { | ||
_mint(msg.sender, 1); | ||
} | ||
// @notice Returns the pinned URI index or the last token URI index (length - 1). | ||
function _getTokenURIIndex(uint256 tokenId) internal view returns (uint256) { | ||
return _hasPinnedTokenURI[tokenId] ? _pinnedURIIndices[tokenId] : _tokenURIs[tokenId].length - 1; | ||
} | ||
// @notice Implementation of ERC721.tokenURI for backwards compatibility. | ||
// @inheritdoc ERC721.tokenURI | ||
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { | ||
_requireMinted(tokenId); | ||
uint256 index = _getTokenURIIndex(tokenId); | ||
string[] memory uris = _tokenURIs[tokenId]; | ||
string memory uri = uris[index]; | ||
// Revert if no URI is found for the token. | ||
require(bytes(uri).length > 0, "ERC721: not URI found"); | ||
return uri; | ||
} | ||
/// @inheritdoc IERC721MultiMetadata.tokenURIs | ||
function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris) { | ||
_requireMinted(tokenId); | ||
return (_getTokenURIIndex(tokenId), _tokenURIs[tokenId]); | ||
} | ||
/// @inheritdoc IERC721MultiMetadata.pinTokenURI | ||
function pinTokenURI(uint256 tokenId, uint256 index) external { | ||
require(msg.sender == ownerOf(tokenId), "Unauthorized"); | ||
_pinnedURIIndices[tokenId] = index; | ||
_hasPinnedTokenURI[tokenId] = true; | ||
emit TokenUriPinned(tokenId, index, msg.sender); | ||
} | ||
/// @inheritdoc IERC721MultiMetadata.unpinTokenURI | ||
function unpinTokenURI(uint256 tokenId) external { | ||
require(msg.sender == ownerOf(tokenId), "Unauthorized"); | ||
_pinnedURIIndices[tokenId] = 0; | ||
_hasPinnedTokenURI[tokenId] = false; | ||
emit TokenUriUnpinned(tokenId, msg.sender); | ||
} | ||
/// @inheritdoc IERC721MultiMetadata.hasPinnedTokenURI | ||
function hasPinnedTokenURI(uint256 tokenId) external view returns (bool isPinned) { | ||
return _hasPinnedTokenURI[tokenId]; | ||
} | ||
/// @notice Sets a specific metadata URI for a token at the given index. | ||
function setUri(uint256 tokenId, uint256 index, string calldata uri) external onlyOwner { | ||
if (_tokenURIs[tokenId].length > index) { | ||
_tokenURIs[tokenId][index] = uri; | ||
} else { | ||
_tokenURIs[tokenId].push(uri); | ||
} | ||
// Emit a MetadataUpdate event (see EIP-4906). | ||
emit MetadataUpdate(tokenId); | ||
} | ||
// Overrides supportsInterface to include IERC721MultiMetadata interface support. | ||
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) { | ||
return ( | ||
interfaceId == type(IERC721MultiMetadata).interfaceId || | ||
super.supportsInterface(interfaceId) | ||
); | ||
} | ||
} | ||
``` | ||
|
||
## Security Considerations | ||
|
||
Care should be taken when specifying access controls for state changing events, such as those that allow uris to be added to tokens | ||
and those specified in this standard: the `pinTokenUri` and `unpinTokenUri` functions. This is up to the developers to specify | ||
as each application may have different requirements to allow for pinning and unpinning. | ||
|
||
## Copyright | ||
|
||
Copyright and related rights waived via [CC0](../LICENSE.md). |