[ERC7160] NFTに複数のメタデータを紐づける仕組みを理解しよう！

# はじめに

初めまして。
**CryptoGames**というブロックチェーンゲーム企業でエンジニアをしている **cardene（かるでね）** です！
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

https://cryptogames.co.jp/

代表的なゲームは**クリプトスペルズ**というブロックチェーンゲームです。

https://cryptospells.jp/

今回は、ERC721の互換性を維持しながら、NFTに複数のメタデータを紐付ける仕組みを提案している規格である**ERC7160**についてまとめていきます！

:::note info
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-7160

他にも様々なERCについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

# 概要

このEIP（イーサリアム改善提案）は、**ERC721**標準に新しい機能を加えることを提案しています。
この提案の主な内容は、1つのトークンに複数のメタデータURIを紐付ける機能です。

:::note info
**ERC721**については以下を参考にしてください。
:::

https://chaldene.net/erc721

新しいインターフェース `IERC721MultiMetadata`が導入され、このインターフェースを通じてトークンに関連付けられた複数のメタデータURIにアクセスすることができます。
このインターフェースには、特定のURIを優先して参照するための「**ピン留めされたURIインデックス**」と、トークンに関連する全てのメタデータURIのリストを取得するメソッドが含まれます。

この提案の重要なポイントは、新機能が既存の**ERC721Metadata**と後方互換性を持っていることです。
つまり、現在**ERC721**標準に基づいて動作しているコントラクトは、新しい機能を追加しても、これまで通りの機能を維持しつつ運用を続けることが可能です。
これにより、既存のコントラクトの運用に影響を与えることなく、新しい拡張機能を利用することができるようになります。

# 動機

現在の **ERC721** 標準では、1つのトークンに対して1つのメタデータURIしか持てませんが、複数のメタデータURIが必要な場面があります。
この新しい拡張機能は、そういった状況をサポートするために提案されました。
以下のようなシチュエーションが想定されます。

- 1つのトークンが、個々に異なるメタデータを持つ複数の資産（例えば、サイクリング用具など）のコレクションを表す場合。
- トークンのメタデータの改訂履歴を、ブロックチェーン上で管理する場合。
- 異なるアスペクト比のメタデータを追加し、あらゆる画面サイズで適切に表示されるようにする場合。
- メタデータが時間と共に変化する場合。
- 複数のアーティストが協力して作るトークンの場合。

これらのシチュエーションを可能にするために、複数のメタデータURIをサポートする機能がこの拡張機能で導入されています。

この複数メタデータ標準を**ERC721Metadata**に追加する主な目的は、現在のdappsやマーケットプレイスがトークンのすべてのURIを表示するメカニズムを持っていないためです。
コレクターがメタデータの中から1つを選び、ピン留めするか解除する標準的な方法を提供することで、この新機能の迅速で容易な導入が可能になります。

# 仕様

この規格は、**ERC721**コントラクトにおけるマルチメタデータ拡張機能について説明し、この拡張機能は、**ERC721**コントラクトにとって任意であり、実装される時は**ERC4906**標準と一緒に使用することが推奨されています。

:::note info
**ERC4906**については以下を参考にしてください。
:::

https://qiita.com/cardene/items/a1f334a059f08971cfaa

```javascript
/// @title EIP-721 Multi-Metdata Extension
/// @dev The ERC-165 identifier for this interface is 0x06e1bc5b.
interface IERC7160 {

  /// @dev This event emits when a token uri is pinned and is
  ///  useful for indexing purposes.
  event TokenUriPinned(uint256 indexed tokenId, uint256 indexed index);

  /// @dev This event emits when a token uri is unpinned and is
  ///  useful for indexing purposes.
  event TokenUriUnpinned(uint256 indexed tokenId);

  /// @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
  /// @dev This call MUST revert if the token does not exist
  /// @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
  /// @return pinned A boolean showing if the token has pinned metadata or not
  function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris, bool pinned);

  /// @notice Pin a specific token uri for a particular token
  /// @dev This call MUST revert if the token does not exist
  /// @dev This call MUST emit a `TokenUriPinned` event
  /// @dev This call MAY emit a `MetadataUpdate` event from ERC-4096
  /// @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 call MUST revert if the token does not exist
  /// @dev This call MUST emit a `TokenUriUnpinned` event
  /// @dev This call MAY emit a `MetadataUpdate` event from ERC-4096
  /// @dev It is up to the developer to define what this function does and is intentionally left open-ended
  /// @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 This call MUST revert if the token does not exist
  /// @dev Useful for on-chain mechanics that don't require the tokenURIs themselves
  /// @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);
}
```

## イベント

---

### TokenUriPinned

```javascript
event TokenUriPinned(uint256 indexed tokenId, uint256 indexed index);
```

**概要**

トークンURIがピン留めされた時に発行されるイベント。

**詳細**

このイベントは、トークンのURIがピン留めされた時に発行され、インデックス目的で役立ちます。
これにより、特定のトークンのピン留めされたURIが追跡され、インデックス化されます。

**パラメータ**

- `tokenId`
    - ピン留めされたURIを持つトークンのID。
- `index`
    - ピン留めされたURIのインデックス。

---

### TokenUriUnpinned

```javascript
event TokenUriUnpinned(uint256 indexed tokenId);
```

**概要**

トークンURIのピン留めが解除された時に発行されるイベント。

**詳細**

このイベントは、トークンのURIのピン留めが解除された時に発行され、インデックス目的で役立ちます。
これにより、特定のトークンのピン留め解除されたURIが追跡され、インデックス化されます。

**パラメータ**

- `tokenId`
    - ピン留め解除されたURIを持つトークンのID。

---

## 関数

### `tokenURIs`

```javascript
function tokenURIs(uint256 tokenId) external view returns (uint256 index, string[] memory uris, bool pinned);
```

**概要**

特定のトークンに関連付けられた全てのURIを取得する関数。

**詳細**

この関数は、指定されたトークンIDに関連する全てのURIを返します。
もしトークンURIがピン留めされている場合、返されるインデックスは文字列配列内の該当するインデックスになるべきです。
トークンが存在しない場合、この関数は失敗する必要があります。

**引数**

- `tokenId`
    - NFTの識別子。

**戻り値**

- `index`
    - ピン留めされたURIのインデックス（ピン留めされていない場合はデフォルトのURI）。
- `uris`
    - トークンに関連付けられた全URIの文字列配列。
- `pinned`
    - トークンにメタデータがピン留めされているかどうかを示すブール値。

---

### `pinTokenURI`

```javascript
function pinTokenURI(uint256 tokenId, uint256 index) external;
```

**概要**

特定のトークンの特定のURIをピン留めする関数。

**詳細**

この関数は、特定のトークンに対して特定のURIをピン留めします。
トークンが存在しない場合、この関数は失敗します。
この関数は`TokenUriPinned`イベントを発行する必要があります。
また、**ERC4096**からの`MetadataUpdate`イベントを発行する可能性があります。

**引数**

- `tokenId`
    - NFTの識別子。
- `index`
    - `tokenURIs` 関数から返される文字列配列内で、トークンにピン留めするべきURIのインデックス。

---

### `unpinTokenURI`

```javascript
function unpinTokenURI(uint256 tokenId) external;
```

**概要**

特定のトークンのメタデータのピン留めを解除する関数。

**詳細**

この関数は、特定のトークンのメタデータのピン留めを解除します。
トークンが存在しない場合、この関数は失敗する必要があります。
この関数は `TokenUriUnpinned`イベントを発行する必要があります。
また、**ERC4096**からの`MetadataUpdate`イベントを発行する可能性があります。
この関数の具体的な動作は開発者に委ねられています。

**引数**

- `tokenId`
    - NFTの識別子。

---

### `hasPinnedTokenURI`

```javascript
function hasPinnedTokenURI(uint256 tokenId) external view returns (bool pinned);
```

**概要**

トークンIDがピン留めされたURIを持っているかどうかをチェックする関数。

**詳細**

この関数は、特定のトークンIDに対して、ピン留めされたURIがあるかどうかを確認します。
トークンが存在しない場合、この関数は失敗する必要があります。
この関数は、トークンURI自体が不要であるオンチェーンのメカニクスに役立ちます。

**引数**

- `tokenId`
    - NFTの識別子。

**戻り値**

- `pinned`
    - トークンにメタデータがピン留めされているかどうかを示すブール値。

---

### `pinTokenURI`関数の使用
トークンURIをピン留めするためにこの関数が使用される時、`TokenUriPinned`イベントが必ず発行されなければなりません。

### `unpinTokenURI` 関数の使用
トークンURIのピン留めを解除するためにこの関数が使用される時、`TokenUriUnpinned`イベントが必ず発行されなければなりません。

### `tokenURI` 関数の役割
**ERC721 Metadata**拡張で定義された `tokenURI`関数は、トークンにピン留めされたURIがある場合そのURIを返す必要があります。
トークンにピン留めされたURIがない場合は、デフォルトのURIを返す必要があります。

### `supportsInterface`メソッド
このメソッドは、`0x06e1bc5b`と呼ばれるインターフェース識別子に対して`true`を返さなければなりません。

### トークンURIの追加と削除機能
トークンにURIを追加または削除する機能は、この標準から別途に実装される必要があります。
URIの追加または削除が行われる際には、**ERC4906**で定義されたイベントの一つが発行されることが推奨されます。

# 補足

この機能は、NFTの所有者がどのメタデータを表示するか選べるように「**ピン留め**」と「**ピン留め解除**」という新しい概念を導入しています。
最初はこれらの機能を各開発者に任せようとしましたが、後にピン留めとピン留め解除のための標準インターフェースを提供することで、dappsがマルチメタデータトークンを簡単にサポートできるようにしました。

`tokenURIs`関数については、最初は文字列配列だけを返すことを考えていましたが、必要な情報を一回の呼び出しで取得できるように、追加情報を含めることにしました。
ピン留めされたURIはトークンの主要なURIとして使用され、メタデータURIのリストはトークン内の個々の資産のメタデータにアクセスするために使われます。
dappsはこれらをギャラリーやメディアカルーセルとして表示することができます。

この拡張機能に含まれる`TokenUriPinned`と`TokenUriUnpinned`イベントは、dappsがどのメタデータを表示するかをインデックスするために使用できます。
これにより、オンチェーンのやり取りが不要になり、イベント駆動型のアーキテクチャを使用することができます。

トークンからURIを追加または削除する際に**ERC4906**の使用を推奨する理由は、このイベントに広範なdappsのサポートがあり、トークンのメタデータが更新されたことをdappsに知らせる必要があるためです。
重複するイベントによるdappsの問題を避けるために、この方法が採用されました。
このイベントを監視している第三者は、更新されたメタデータを取得するために`tokenURIs`関数を呼び出すことができます。

# 後方互換性

このマルチメタデータ拡張機能は、既にある**ERC721**コントラクトとの互換性を保つために設計されています。
これは、`tokenURI`メソッドが、トークンURIがピン留めされていればそのピン留めされたURIを、ピン留めされていなければデフォルトのURIを返すように実装されるべきだということを意味します。
この仕組みにより、すでに存在する**ERC721**コントラクトも、新しいマルチメタデータ機能を利用することが可能になります。

# 参考実装

`IERC721MultiMetadata`インターフェースのオープンソースの参照実装が提供されることがあります。
これは、既存の**ERC721**コントラクトを拡張してマルチメタデータ機能をサポートする方法を示すものです。
この参照実装は、自分のコントラクトにこの拡張機能を実装しようとする開発者のためのガイドとして機能します。

```javascript
// 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 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, 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 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 MetadataUpdate(tokenId);
  }

  // Overrides supportsInterface to include IERC721MultiMetadata interface support.
  function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
    return (
      interfaceId == type(IERC7160).interfaceId ||
      super.supportsInterface(interfaceId)
    );
  }
}
```

# セキュリティ考慮事項

トークンにURIを追加するなど、状態を変更するイベントや、この標準で規定されている`pinTokenUri`および`unpinTokenUri`関数のアクセス制御については、慎重に設定する必要があります。
ピン留めやピン留め解除を行うための条件は、それぞれのアプリケーションによって異なる可能性があるため、これらの制御設定は開発者が自身で行う必要があります。

:::note info
これらの機能をどのように利用できるようにするかは、各コントラクトを開発する人が決めることになります。
:::

# 引用

0xG (@0xGh), Marco Peyfuss (@mpeyfuss), "ERC-7160: ERC-721 Multi-Metadata Extension," Ethereum Improvement Proposals, no. 7160, June 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7160.























# 最後に

今回は「ERC721の互換性を維持しながら、NFTに複数のメタデータを紐付ける仕組みを提案している規格である**ERC7160**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4