Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EIP: ERC-721 Multi-Metadata Extension #7160

Merged
merged 16 commits into from
Jul 11, 2023
163 changes: 163 additions & 0 deletions EIPS/eip-7160.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
eip: 7160
title: ERC-721 Multi-Metadata Extension
description: "This EIP proposes an extension to the ERC-721 to support multiple metadata URIs per token."
0xGh marked this conversation as resolved.
Show resolved Hide resolved
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. However, there are use cases where multiple metadata URIs are desirable, such as when a token represents a collection of (cycling) assets with individual metadata, historic token metadata, collaborative and multi-artist tokens, evolving tokens. This extension enables such use cases by introducing the concept of multi-metadata support.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be important to include answers to:

  • Why does this need to be more than just tokenURI and some token-specific functions to change the active URI?
  • Who would be interested in the non-pinned URIs?
  • How would this information be displayed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a paragraph right below. Let me know what you think. Also @mpeyfuss can chime in and add further reasons for why having a multi-metadata standard might be a good idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah there are quite a few examples we've found working with creators and collectors on why multi-metadata is interesting. Can add example use cases. One of the most mentioned reasons is that of having the option to provide versions of the token that match different aspect ratios and could be utilized for specific display purposes while keeping provenance/ownership intact.


## Specification

The `IERC721MultiMetadata` interface extends the existing `IERC721` interface and introduces additional methods and events:

```solidity
interface IERC721MultiMetadata is IERC721 {
0xGh marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm no Solidity expert, but I've been told to avoid making interfaces depend on one another:

Suggested change
interface IERC721MultiMetadata is IERC721 {
interface IERC721MultiMetadata /* is IERC721 */ {

Copy link
Contributor Author

@0xGh 0xGh Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented out as suggested by you. By the way should the interface name reflect the EIP name (IERC7160) instead of being called IERC721MultiMetadata or is it fine to give it a name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can name it however you like!

event TokenUriPinned(uint256 indexed tokenId, uint256 indexed index, address indexed sender);
event TokenUriUnpinned(uint256 indexed tokenId, address indexed sender);

function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris);
Copy link
Contributor Author

@0xGh 0xGh Jun 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SamWilsn initially my idea was to propose only the addition of the tokenURIs method as it would allow dapps to retrieve all the uris and the current index (whether it is pinned or not).

However in this proposal we provide a standard interface to dapps/marketplaces to implement pinning, unpinning and related events. Should they decide to adopt this EIP I assume that it would make sense for them to have a standard way to offer pinning/unpinning via UI.

Thoughts on these additional methods and events for pinning/unpinning? Should we keep or drop them?

By the way we'll try to get their (marketplaces) opinion too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0xGh don't the functions on lines 34 and 35 accomplish this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes they do. Just sharing some context and thoughts based on what we discussed in private.

function pinTokenURI(uint256 tokenId, uint256 index) external;
function unpinTokenURI(uint256 tokenId) external;
function hasPinnedTokenURI(uint256 tokenId) external view returns (bool pinned);
}
```

The `tokenURIs` function returns a tuple containing the pinned URI index and a list of all metadata URIs associated with the specified token.

`tokenURIs` is the core method of a multi-metadata token and the additional methods and events are meant to provide a standard interface to dapps to perform pinning and unpinning actions.

The `pinTokenURI` function allows the contract owner to designate a specific metadata URI as the pinned URI for a token.

The `unpinTokenURI` function allows the contract owner to remove the pin on a specific URI index.

The `tokenURI` function defined in the ERC-721 standard must return the pinned URI or the last URI in the list returned by `tokenURIs` when there is not pinned URI. This ensures backwards compatibility with existing contracts and applications that rely on the single metadata URI.

When adding new URIs it is highly encouraged to implement [ERC-4906](./eip-4906.md) and emit a `MetadataUpdate` event.

See the [Implementation](#reference-implementation) section for an example.

## Rationale

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 last URI in the list returned by tokenURIs or the pinned URI.

## 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: MIT
0xGh marked this conversation as resolved.
Show resolved Hide resolved

pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC4906.sol";
import "./IERC721MultiMetadata.sol";

contract MultiMetadata is Ownable, ERC721, IERC721MultiMetadata, IERC4906 {
mapping(uint256 => string[]) _tokenURIs;
mapping(uint256 => uint256) _pinnedURIIndices;
mapping(uint256 => bool) _hasPinnedTokenURI;

constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {
_mint(msg.sender, 1);
}

// 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;
}

// Implementation of ERC721.tokenURI for backwards compatibility.
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;
}

// Retrieves the pinned URI index and the list of all metadata URIs associated with the specified token.
function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris) {
_requireMinted(tokenId);
return (_getTokenURIIndex(tokenId), _tokenURIs[tokenId]);
}

// Sets a specific metadata URI as the pinned URI for a token.
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);
}

// Unsets the pinned URI for a token.
function unpinTokenURI(uint256 tokenId) external {
require(msg.sender == ownerOf(tokenId), "Unauthorized");
_pinnedURIIndices[tokenId] = 0;
_hasPinnedTokenURI[tokenId] = false;
emit TokenUriUnpinned(tokenId, msg.sender);
}

// Checks if a token has a pinned URI.
function hasPinnedTokenURI(uint256 tokenId) external view returns (bool isPinned) {
require(msg.sender == ownerOf(tokenId), "Unauthorized");
return _hasPinnedTokenURI[tokenId];
}

// 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

This extension does not poses new security risks outside of those that might arise from existing token URI handling practices.

## Copyright

This work is licensed under the MIT License.
0xGh marked this conversation as resolved.
Show resolved Hide resolved