Alert Source Discuss
Standards Track: ERC

ERC-7160: ERC-721 多元数据扩展

每个 token 多个元数据 URI,可以选择固定一个主 URI。

Authors 0xG (@0xGh), Marco Peyfuss (@mpeyfuss)
Created 2023-06-09
Requires EIP-165, EIP-721

摘要

本 EIP 提出对 ERC-721 标准的扩展,以支持每个 token 多个元数据 URI。它引入了一个新的接口 IERC721MultiMetadata,该接口提供了访问与 token 关联的元数据 URI 的方法,包括固定的 URI 索引和所有元数据 URI 的列表。该扩展旨在与现有的 ERC721Metadata 实现向后兼容。

动机

当前的 ERC-721 标准允许每个 token 使用 ERC721Metadata 实现的单个元数据 URI。但是,在某些用例中,需要多个元数据 URI。下面列出了一些示例用例:

  • 一个 token 代表一组具有单独元数据的(循环)资产
  • token 元数据修订的链上历史记录
  • 附加具有不同纵横比的元数据,以便可以在所有屏幕上正确显示
  • 动态和不断发展的元数据
  • 协作和多艺术家 token

此扩展通过引入多元数据支持的概念来启用此类用例。

除了现有的 ERC721Metadata 标准之外,拥有一个多元数据标准的主要原因是 dapps 和市场没有一种机制来推断和显示所有 token URI。为市场提供一种标准方式,让收藏家可以选择固定/取消固定元数据选项之一,也可以快速轻松地采用此功能。

规范

本文档中的关键词“必须(MUST)”、“禁止(MUST NOT)”、“必需(REQUIRED)”、“应该(SHALL)”、“不应该(SHALL NOT)”、“推荐(SHOULD)”、“不推荐(SHOULD NOT)”、“可以(MAY)”和“可选(OPTIONAL)”应按照 RFC 2119 中的描述进行解释。

多元数据扩展对于 ERC-721 合约是可选的,如果实现,建议与 ERC-4906 标准结合使用

/// @title EIP-721 多元数据扩展
/// @dev 此接口的 ERC-165 标识符为 0x06e1bc5b。
interface IERC7160 {

  /// @dev 当 token uri 被固定时,会发出此事件
  ///  对索引编制很有用。
  event TokenUriPinned(uint256 indexed tokenId, uint256 indexed index);

  /// @dev 当 token uri 被取消固定时,会发出此事件
  ///  对索引编制很有用。
  event TokenUriUnpinned(uint256 indexed tokenId);

  /// @notice 获取与特定 token 关联的所有 token uri
  /// @dev 如果 token uri 被固定,则返回的索引应该是字符串数组中的索引
  /// @dev 如果 token 不存在,则此调用必须恢复
  /// @param tokenId nft 的标识符
  /// @return index 一个 unisgned 整数,用于指定为 token 固定哪个 uri(如果未固定,则为默认 uri)
  /// @return uris 与 token 关联的所有 uri 的字符串数组
  /// @return pinned 一个布尔值,显示该 token 是否具有固定的元数据
  function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris, bool pinned);

  /// @notice 为特定 token 固定一个特定的 token uri
  /// @dev 如果 token 不存在,则此调用必须恢复
  /// @dev 此调用必须发出一个 `TokenUriPinned` 事件
  /// @dev 此调用可以发出 ERC-4096 中的 `MetadataUpdate` 事件
  /// @param tokenId nft 的标识符
  /// @param index 从 `tokenURIs` 函数返回的字符串数组中的索引,该索引应为 token 固定
  function pinTokenURI(uint256 tokenId, uint256 index) external;

  /// @notice 取消固定特定 token 的元数据
  /// @dev 如果 token 不存在,则此调用必须恢复
  /// @dev 此调用必须发出一个 `TokenUriUnpinned` 事件
  /// @dev 此调用可以发出 ERC-4096 中的 `MetadataUpdate` 事件
  /// @dev 由开发人员定义此函数的功能,并故意保持开放性
  /// @param tokenId nft 的标识符
  function unpinTokenURI(uint256 tokenId) external;

  /// @notice 在链上检查 token id 是否具有固定的 uri
  /// @dev 如果 token 不存在,则此调用必须恢复
  /// @dev 适用于不需要 tokenURIs 本身的链上机制
  /// @param tokenId nft 的标识符
  /// @return pinned 一个布尔值,用于指定 token 是否具有固定的元数据
  function hasPinnedTokenURI(uint256 tokenId) external view returns (bool pinned);
}

使用 pinTokenUri 函数固定 token uri 时,必须发出 TokenUriPinned 事件。

使用 unpinTokenUri 函数取消固定 token uri 时,必须发出 TokenUriUnpinned 事件。

当 token 具有固定的 uri 时,ERC-721 元数据扩展中定义的 tokenURI 函数必须返回固定的 URI。

当 token 具有未固定的 uri 时,ERC-721 元数据扩展中定义的 tokenURI 函数必须返回默认 uri。

当使用 0x06e1bc5b 调用时,supportsInterface 方法必须返回 true

向 token 添加或从中删除 uri 的功能的实现必须与此标准分开实现。建议在添加或删除 uri 时发出 ERC-4906 中定义的事件之一。

有关示例,请参见 实现部分。

理由

为了保持获取元数据的通用性,使用了与 ERC-721 类似的术语。引入了固定和取消固定元数据的概念,因为很明显,NFT 所有者可能希望选择要显示的元数据。起初,我们考虑将固定和取消固定操作留给每个开发人员,但意识到固定和取消固定操作的标准接口允许 dApp 轻松实现对多元数据 token 的通用支持。

我们首先考虑 tokenURIs 函数是否应该只返回一个字符串数组,但添加了额外的信息,以便您可以通过一次调用而不是可能的三次调用来获取所有所需的信息。 固定的 URI 应用作 token 的主 URI,而元数据 URI 列表可用于访问 token 中各个资产的元数据。 dApp 可以将这些显示为图库或媒体轮播。

dApp 可以使用本规范中包含的 TokenUriPinnedTokenUriUnpinned 事件来索引显示哪些元数据。 这可以消除链上调用,并且可以改为使用事件驱动的架构。

本标准建议在从 token 添加或删除 uri 时使用 ERC-4906 的原因是,dApp 已经对该事件提供了广泛的支持,并且它已经是所需的事件 - 提醒 dApp token 的元数据已更新。 我们不想由于重复事件而可能导致 dApp 问题。 然后,收听此事件的第三方可以调用 tokenURIs 函数以获取更新的元数据。

向后兼容性

此扩展旨在与现有的 ERC-721 合约向后兼容。 tokenURI 方法的实现必须返回固定的 token uri(如果已固定)或某些默认 uri(如果未固定)。

参考实现

可以提供 IERC721MultiMetadata 接口的开源参考实现,演示如何扩展现有的 ERC-721 合约以支持多元数据功能。 此参考实现可以作为开发人员在其自己的合约中实现扩展的指南。

// 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 {IERC7160} from "./IERC7160.sol";

contract MultiMetadata is ERC721, Ownable, IERC7160, 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 返回固定的 URI 索引或最后一个 token URI 索引(长度 - 1)。
  function _getTokenURIIndex(uint256 tokenId) internal view returns (uint256) {
    return _hasPinnedTokenURI[tokenId] ? _pinnedURIIndices[tokenId] : _tokenURIs[tokenId].length - 1;
  }

  // @notice 为向后兼容性实现 ERC721.tokenURI。
  // @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];

    // 如果找不到 token 的 URI,则恢复。
    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, bool pinned) {
    _requireMinted(tokenId);
    return (_getTokenURIIndex(tokenId), _tokenURIs[tokenId], _hasPinnedTokenURI[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);
  }

  /// @inheritdoc IERC721MultiMetadata.unpinTokenURI
  function unpinTokenURI(uint256 tokenId) external {
    require(msg.sender == ownerOf(tokenId), "Unauthorized");
    _pinnedURIIndices[tokenId] = 0;
    _hasPinnedTokenURI[tokenId] = false;
    emit TokenUriUnpinned(tokenId);
  }

  /// @inheritdoc IERC721MultiMetadata.hasPinnedTokenURI
  function hasPinnedTokenURI(uint256 tokenId) external view returns (bool pinned) {
    return _hasPinnedTokenURI[tokenId];
  }

  /// @notice 为给定索引处的 token 设置特定的元数据 URI。
  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 MetadataUpdate(tokenId);
  }

  // 覆盖 supportsInterface 以包含 IERC721MultiMetadata 接口支持。
  function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
    return (
      interfaceId == type(IERC7160).interfaceId ||
      super.supportsInterface(interfaceId)
    );
  }
}

安全注意事项

在指定状态更改事件(例如允许将 uri 添加到 token 的事件)的访问控制以及本标准中指定的事件(pinTokenUriunpinTokenUri 函数)时,应格外小心。 这取决于开发人员来指定,因为每个应用程序可能具有不同的固定和取消固定要求。

版权

CC0 下放弃版权及相关权利。

Citation

Please cite this document as:

0xG (@0xGh), Marco Peyfuss (@mpeyfuss), "ERC-7160: ERC-721 多元数据扩展," Ethereum Improvement Proposals, no. 7160, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7160.