# [ERC5773] NFTの提供形式を画像・PDF・3Dモデルに切り替えたり、内容をアップデートする仕組みを理解しよう！

> マルチアセットNFT規格は、デジタルアセットをより柔軟に、かつ多様な形式で表現できる方法を提供するもので、個々のNFTに合わせて異なる情報を表示する方法を提供します。

- URL: https://cardene.dev/articles/ec6641365505a2c06964
- 公開日: 2023-08-24
- 更新日: 2023-08-24
- トピック: Blockchain
- タグ: ethereum, solidity, ブロックチェーン, nft

## はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、アクセスされる媒体によって提供するNFTを切り替えることで、NFTの提供形式を変えることができる提案である**ERC5773**についてまとめていきます！

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

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

## 概要

マルチアセットNFT規格は、デジタルアセットをより柔軟に、かつ多様な形式で表現できる方法を提供するもので、個々のNFTに合わせて異なる情報を表示する方法を提供します。

この「**コンテキストに応じた情報の出力**」とは、NFTがどのようにアクセスされるかに応じて、最適な形式のアセットを表示することを指します。
たとえば、もしNFTが電子書籍リーダーで開かれるなら、PDF形式のアセットが表示され、マーケットプレイスで見られる場合はPNGやSVG形式のアセットが表示されます。
また、ゲーム内からアクセスされる場合は3Dモデルが表示され、IoTハブからアクセスされる場合は必要なアドレッシングや仕様情報を提供するアセットが取得されます。

:::message
アドレッシングは、コンピュータシステムやネットワーク内で、特定のリソースやデータの位置や識別子を示すために使用される手段です。
アドレッシングは、通常、データやリソースを特定するための情報を含む一意の識別子や場所の指定方法を指します。

IoT（Internet of Things）の文脈では、アドレッシングは物理的なデバイスやセンサー、アクチュエータなどに関連する情報を指します。
IoTハブからアクセスされる場合には、NFTが関連するIoTデバイスの位置、接続情報、通信プロトコル、センサーデータの解釈方法などのアドレッシング情報が提供されるアセットが取得されます。

このアセットに含まれるアドレッシング情報によって、IoTデバイスがNFTに関連する情報や命令を適切に取得し、処理することが可能になります。
アドレッシングは、IoTの世界においてデバイス間の連携やデータの取得、制御を効果的に行うために重要な概念です。
:::

NFTは複数のアセット（情報の出力）を持つことができます。
これらのアセットは、消費者に提供されるファイルであり、重要度の順に並べられます。
これらのアセットは、`mimetype`（ファイルの種類）や`tokenURI`（トークンの情報アドレス）が一致する必要はありませんし、互いに影響を与える必要もありません。
これらのアセットは、トークンの所有者が任意の順序で設定できる「**namespaced tokenURIs（名前空間付きのtokenURI）**」と考えると分かりやすいでしょう。
ただし、アセットの変更、更新、追加、削除は、NFTの所有者と発行者の双方が合意した場合にのみ行われることに注意してください。

## 動機

NFT（非代替トークン）は、Ethereumエコシステム内で広く使用されるデジタルトークンで、さまざまな用途に利用されています。
この提案は、NFTに新たな使い方や価値を加えるための方法を標準化することを目指しています。
1つのNFTに複数の異なるアセットを関連付けることにより、より多くの便益や使いやすさ、将来の互換性を実現できるようになります。

**ERC721**規格が公開されてからの4年間で、NFTに追加の機能が求められ、多くの拡張が行われてきました。
このEIP（Ethereum Improvement Proposal）は、**ERC721**規格を以下の点で改善することを提案しています。

1. **クロスメタバースの互換性**
メタバースの概念はまだ成熟していないため、将来のメタバースの進化に対応できる柔軟性を持つことが重要です。
これにより、さまざまな異なる実装に対応できるようになります。

2. **マルチメディアの出力**
NFTがさまざまな形式のメディアを持つことで、多様なプラットフォームやアプリケーションで活用される可能性が広がります。
例えば、ゲーム内のコスメアイテムを示すアセットや、マーケットプレース向けのスタイリッシュなサムネイルなどが含まれます。

3. **メディアの冗長性**
同じ情報やアセットが複数の方法で提供されることで、冗長性を持たせることができます。
これにより、情報の失われるリスクを低減し、より確実な表示や利用が可能になります。

4. **NFTの進化**
NFT自体も進化する可能性があり、新たな情報や機能を追加する際に、柔軟に対応できる仕組みを提供します。

この提案の中でも特に重要な要素は、「**クロスメタバースの互換性**」と「**マルチメディアの出力**」です。
クロスメタバースの互換性は、さまざまなゲームやプラットフォーム間での異なる環境でもアセットを活用できるようにします。
マルチメディアの出力は、NFTが画像、動画、音声などさまざまな形式のメディアを持ち、幅広いコンテンツに適用できるようにします。

この提案は、NFTの将来的な可能性を広げるための重要な一歩であり、より使いやすく、柔軟なNFTの世界を築くための取り組みです。

### クロスメタバースの互換性

この提案では、NFTに新たな有用性と柔軟性をもたらす考え方が提案されています。
まず、「**クロスメタバースの互換性**」とは、将来の進化によって定義が変わる可能性がある「**メタバース**」という用語に関連しています。
メタバースはまだ明確に定義されていない概念ですが、どのように進化するかに関わらず、この提案は異なる実装をサポートできるように設計されています。

クロスメタバースの互換性は、異なるメタバースやゲームエンジン間での互換性を指します。
例えば、ゲームAでのアイテムが、ゲームBでは利用できないケースがあります。
これは、ゲームエンジンの違いによる互換性の問題です。

こうしたNFTには、新たな追加アセットを組み合わせることで、さらなる有用性を付与することができます。
例えば、異なるゲームやアイテムを同じNFTに関連付けることで、1つのゲームアイテムが、さまざまな状況やプラットフォームで利用可能な進化するNFTに変わります。

あなたがゲームAとゲームBという2つの異なるゲームをプレイしているとします。
それぞれのゲームには異なるキャラクターやアイテムが存在します。
このとき、ゲームAで手に入れたカッコいい剣や盾を、ゲームBでも使いたいと思ったとします。

ここで、クロスメタバースの互換性の概念が登場します。
つまり、ゲームAの剣や盾を、ゲームBでも利用できるようにする仕組みです。
これにより、異なるゲーム間でアイテムを共有することができるようになります。

では、具体的なアセットの例を考えてみましょう。
ゲームAで手に入れたカッコいい剣は、1つのアセットです。
そのアセットには、剣の見た目や特性が含まれています。
同様に、ゲームBでも利用できるようにするために、もう1つのアセットを用意します。
このアセットにも、ゲームBでの剣の情報が含まれています。
そして、3つ目のアセットは、どのゲームでも、カタログやマーケットプレース、NFTビューアなどで表示される汎用的な情報を持っています。
これには、剣の見た目を示すスタイリッシュなサムネイルや、アニメーション付きのデモやトレーラーが含まれます。
このアセットによって、他の人がその剣の魅力を知ることができます。

この提案は、ゲーム開発者がユーザーのNFTからアセットデータを簡単に取得できるようにするための抽象化のレイヤーを追加します。
これにより、ハードコーディングする必要なく、より柔軟で多様なアプリケーションや体験を提供できるようになります。

### マルチメディアの出力

例えば、電子書籍のNFTを考えてみましょう。
このNFTは、電子書籍の形式によって異なるファイル形式で表示されます。
電子書籍リーダーアプリで読み込むと、PDFファイルとして表示されます。
同じNFTをオーディオブックアプリで読み込むと、代わりにMP3ファイルが使用され、音声として再生されます。
また、このNFTには電子書籍のタイトルや著者、本の表紙画像などのメタデータも含まれているかもしれません。
これによって、NFTがさまざまな状況や場所で識別され、適切に表示されるようになります。
たとえば、マーケットプレースや検索結果ページ、ポートフォリオトラッカーなどで、NFTに関する情報が適切に表示されるのです。

### メディアの冗長性

NFTが急いで作成される際、セキュリティやデータ管理の方法についてしっかり考えられないことがあります。
このため、NFTのメタデータ（資産の詳細情報や特性）が単一のサーバーに集中して配置されることがあります。
これは、そのサーバーに障害が発生した場合、関連するすべてのNFTの情報が失われるリスクを孕んでいます。

また、一部の場合では、NFTのメタデータへのアクセスを提供するためにIPFS（InterPlanetary File System）ゲートウェイがハードコーディングされることがあります。
IPFSは分散型のファイル共有プロトコルであり、ファイルをユーザーコンピュータ間で分散して保存する仕組みを提供します。
しかし、特定のIPFSゲートウェイがダウンする場合、NFTのメタデータへのアクセスが阻害される可能性があります。

これらのアプローチでは、単一障害点が存在し、耐障害性が低くなります。
1つのサーバーや特定のIPFSゲートウェイがダウンすると、それに関連するNFTの情報へのアクセスができなくなる可能性があるため、ユーザーや所有者は不便を感じることがあります。
また、データの安全性も懸念されます。

この問題に対処するために、同じメタデータファイルを異なるアセットとして複数の場所に追加します。
例えば、同じメタデータと関連する画像を**Arweave**、**Sia**、**IPFS**など、異なるプロトコルに追加することで、メタデータの情報が冗長になり、耐障害性が向上します。
すべてのプロトコルが同時にダウンする可能性は低いため、データが失われるリスクが減少します。
このアプローチによって、NFTのメタデータとその参照情報がより安全に保護され、信頼性の高い運用が実現されるのです。

### NFTの進化

多くのNFT、特にゲーム関連のものは、進化が必要です。
特に現代のメタバースでは、実際には単なるマルチプレイヤーゲームであり、ユーザー名とパスワードのログインをNFTを保有しているかの確認に置き換えたものです。

たとえば、ゲームのサーバーがダウンするか、ゲームが終了すると、プレイヤーは何も持たなくなる（経験値の喪失）か、ゲームとは関係のないアセットやアクセサリーを所有することになります。
このような場合、他の「**メタバース**」と互換性がなく、ウォレットに無関係なアセットが表示されたり、他の「**バース**」との互換性がないアイテムが表示されたりする問題が生じることもあります（前述の「**クロスメタバースの互換性**」を参照）。

マルチアセットNFTを使用すると、発行者や事前承認されたエンティティがNFTの所有者に新しいアセットを提案できます。
所有者は提案を受け入れるか拒否することができます。
提案されたアセットは、既存のアセットを置き換えるために利用されることも可能です。

アセットの置き換えは、ある程度、**ERC721**トークンのURIを置き換えるプロセスに似ています。
アセットが置き換えられると、古いアセットもまだアクセス可能であり、検証可能です。
アセットのメタデータURIを置き換えることによって、アセットの変更履歴が曖昧になることがあります。
また、発行者がNFTのアセットを自由に置き換えることができない場合、トークンの所有者に対する信頼性が向上します。
この提案のアセットの置き換えメカニズムによって、トークンの所有者に対するこの保証が提供されます。

この機能により、経験値を集めた後、ユーザーはレベルアップを受け入れることができます。
レベルアップには、新しいアセットがNFTに追加され、受け入れると古いアセットが新しいアセットに置き換わります。

具体的な例として、ポケモンの進化を考えてみましょう。
トレーナーは、十分な経験値を獲得した後、モンスターを進化させることができます。
マルチアセットNFTを使用すると、アセットを置き換えるために中央集権的な管理が不要であり、ユーザーのウォレットに別のNFTを送信する必要もありません。
代わりに、新しいアセット（例：**Raichu**）が既存のアセット（例：**Pikachu**）に追加され、受け入れると**Pikachu**アセットは消え、**Raichu**に置き換わります。
そして、**Raichu**には独自の属性や価値が設定されます。

また、別の例として、IoTデバイスのファームウェアのバージョン管理が考えられます。
アセットは現在のファームウェアを表し、アップデートが利用可能になると、新しいアセットが現在のアセットと置き換わることで、ファームウェアの更新が行われます。

:::message
**ファームウェア**

ファームウェア（Firmware）は、コンピュータや電子機器などのハードウェアに組み込まれているソフトウェアの一種です。
通常、ファームウェアはハードウェアの制御や動作を管理するためのプログラムや命令の集まりです。
ファームウェアは、デバイスの動作や機能を制御し、ハードウェアとソフトウェアの間のインターフェースを提供します。

具体的には、コンピュータのマザーボード、グラフィックスカード、ネットワークデバイス、ルーター、スマートフォン、デジタルカメラ、家電製品など、さまざまな電子機器やデバイスにファームウェアが含まれています。
ファームウェアは、デバイスの初期化、設定、制御、アップグレードなどの機能を担当し、デバイスの正確な動作を確保する役割を果たします。

IoT（Internet of Things）デバイスなどの場合、ファームウェアはデバイスの動作や通信プロトコル、セキュリティ機能などを制御し、必要に応じてアップデートやアップグレードが行われることで、デバイスの機能やセキュリティの向上が図られます。
:::

## 仕様

```javascript
/// @title ERC-5773 Context-Dependent Multi-Asset Tokens
/// @dev See https://eips.ethereum.org/EIPS/eip-5773
/// @dev Note: the ERC-165 identifier for this interface is 0x06b4329a.

pragma solidity ^0.8.16;

interface IERC5773 /* is ERC165 */ {
    /**
     * @notice Used to notify listeners that an asset object is initialised at `assetId`.
     * @param assetId ID of the asset that was initialised
     */
    event AssetSet(uint64 assetId);

    /**
     * @notice Used to notify listeners that an asset object at `assetId` is added to token's pending asset
     *  array.
     * @param tokenIds An array of IDs of the tokens that received a new pending asset
     * @param assetId ID of the asset that has been added to the token's pending assets array
     * @param replacesId ID of the asset that would be replaced
     */
    event AssetAddedToTokens(
        uint256[] tokenIds,
        uint64 indexed assetId,
        uint64 indexed replacesId
    );

    /**
     * @notice Used to notify listeners that an asset object at `assetId` is accepted by the token and migrated
     *  from token's pending assets array to active assets array of the token.
     * @param tokenId ID of the token that had a new asset accepted
     * @param assetId ID of the asset that was accepted
     * @param replacesId ID of the asset that was replaced
     */
    event AssetAccepted(
        uint256 indexed tokenId,
        uint64 indexed assetId,
        uint64 indexed replacesId
    );

    /**
     * @notice Used to notify listeners that an asset object at `assetId` is rejected from token and is dropped
     *  from the pending assets array of the token.
     * @param tokenId ID of the token that had an asset rejected
     * @param assetId ID of the asset that was rejected
     */
    event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId);

    /**
     * @notice Used to notify listeners that token's priority array is reordered.
     * @param tokenId ID of the token that had the asset priority array updated
     */
    event AssetPrioritySet(uint256 indexed tokenId);

    /**
     * @notice Used to notify listeners that owner has granted an approval to the user to manage the assets of a
     *  given token.
     * @dev Approvals must be cleared on transfer
     * @param owner Address of the account that has granted the approval for all token's assets
     * @param approved Address of the account that has been granted approval to manage the token's assets
     * @param tokenId ID of the token on which the approval was granted
     */
    event ApprovalForAssets(
        address indexed owner,
        address indexed approved,
        uint256 indexed tokenId
    );

    /**
     * @notice Used to notify listeners that owner has granted approval to the user to manage assets of all of their
     *  tokens.
     * @param owner Address of the account that has granted the approval for all assets on all of their tokens
     * @param operator Address of the account that has been granted the approval to manage the token's assets on all of the
     *  tokens
     * @param approved Boolean value signifying whether the permission has been granted (`true`) or revoked (`false`)
     */
    event ApprovalForAllForAssets(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    /**
     * @notice Accepts an asset at from the pending array of given token.
     * @dev Migrates the asset from the token's pending asset array to the token's active asset array.
     * @dev Active assets cannot be removed by anyone, but can be replaced by a new asset.
     * @dev Requirements:
     *
     *  - The caller must own the token or be approved to manage the token's assets
     *  - `tokenId` must exist.
     *  - `index` must be in range of the length of the pending asset array.
     * @dev Emits an {AssetAccepted} event.
     * @param tokenId ID of the token for which to accept the pending asset
     * @param index Index of the asset in the pending array to accept
     * @param assetId Id of the asset expected to be in the index
     */
    function acceptAsset(
        uint256 tokenId,
        uint256 index,
        uint64 assetId
    ) external;

    /**
     * @notice Rejects an asset from the pending array of given token.
     * @dev Removes the asset from the token's pending asset array.
     * @dev Requirements:
     *
     *  - The caller must own the token or be approved to manage the token's assets
     *  - `tokenId` must exist.
     *  - `index` must be in range of the length of the pending asset array.
     * @dev Emits a {AssetRejected} event.
     * @param tokenId ID of the token that the asset is being rejected from
     * @param index Index of the asset in the pending array to be rejected
     * @param assetId Id of the asset expected to be in the index
     */
    function rejectAsset(
        uint256 tokenId,
        uint256 index,
        uint64 assetId
    ) external;

    /**
     * @notice Rejects all assets from the pending array of a given token.
     * @dev Effectively deletes the pending array.
     * @dev Requirements:
     *
     *  - The caller must own the token or be approved to manage the token's assets
     *  - `tokenId` must exist.
     * @dev Emits a {AssetRejected} event with assetId = 0.
     * @param tokenId ID of the token of which to clear the pending array
     * @param maxRejections to prevent from rejecting assets which arrive just before this operation.
     */
    function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external;

    /**
     * @notice Sets a new priority array for a given token.
     * @dev The priority array is a non-sequential list of `uint16`s, where the lowest value is considered highest
     *  priority.
     * @dev Value `0` of a priority is a special case equivalent to uninitialised.
     * @dev Requirements:
     *
     *  - The caller must own the token or be approved to manage the token's assets
     *  - `tokenId` must exist.
     *  - The length of `priorities` must be equal the length of the active assets array.
     * @dev Emits a {AssetPrioritySet} event.
     * @param tokenId ID of the token to set the priorities for
     * @param priorities An array of priorities of active assets. The succession of items in the priorities array
     *  matches that of the succession of items in the active array
     */
    function setPriority(uint256 tokenId, uint64[] calldata priorities)
        external;

    /**
     * @notice Used to retrieve IDs of the active assets of given token.
     * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call
     *  `getAssetMetadata(tokenId, assetId)`.
     * @dev You can safely get 10k
     * @param tokenId ID of the token to retrieve the IDs of the active assets
     * @return uint64[] An array of active asset IDs of the given token
     */
    function getActiveAssets(uint256 tokenId)
        external
        view
        returns (uint64[] memory);

    /**
     * @notice Used to retrieve IDs of the pending assets of given token.
     * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call
     *  `getAssetMetadata(tokenId, assetId)`.
     * @param tokenId ID of the token to retrieve the IDs of the pending assets
     * @return uint64[] An array of pending asset IDs of the given token
     */
    function getPendingAssets(uint256 tokenId)
        external
        view
        returns (uint64[] memory);

    /**
     * @notice Used to retrieve the priorities of the active assets of a given token.
     * @dev Asset priorities are a non-sequential array of uint16 values with an array size equal to active asset
     *  priorites.
     * @param tokenId ID of the token for which to retrieve the priorities of the active assets
     * @return uint16[] An array of priorities of the active assets of the given token
     */
    function getActiveAssetPriorities(uint256 tokenId)
        external
        view
        returns (uint64[] memory);

    /**
     * @notice Used to retrieve the asset that will be replaced if a given asset from the token's pending array
     *  is accepted.
     * @dev Asset data is stored by reference, in order to access the data corresponding to the ID, call
     *  `getAssetMetadata(tokenId, assetId)`.
     * @param tokenId ID of the token to check
     * @param newAssetId ID of the pending asset which will be accepted
     * @return uint64 ID of the asset which will be replaced
     */
    function getAssetReplacements(uint256 tokenId, uint64 newAssetId)
        external
        view
        returns (uint64);

    /**
     * @notice Used to fetch the asset metadata of the specified token's active asset with the given index.
     * @dev Can be overriden to implement enumerate, fallback or other custom logic.
     * @param tokenId ID of the token from which to retrieve the asset metadata
     * @param assetId Asset Id, must be in the active assets array
     * @return string The metadata of the asset belonging to the specified index in the token's active assets
     *  array
     */
    function getAssetMetadata(uint256 tokenId, uint64 assetId)
        external
        view
        returns (string memory);

    /**
     * @notice Used to grant permission to the user to manage token's assets.
     * @dev This differs from transfer approvals, as approvals are not cleared when the approved party accepts or
     *  rejects an asset, or sets asset priorities. This approval is cleared on token transfer.
     * @dev Only a single account can be approved at a time, so approving the `0x0` address clears previous approvals.
     * @dev Requirements:
     *
     *  - The caller must own the token or be an approved operator.
     *  - `tokenId` must exist.
     * @dev Emits an {ApprovalForAssets} event.
     * @param to Address of the account to grant the approval to
     * @param tokenId ID of the token for which the approval to manage the assets is granted
     */
    function approveForAssets(address to, uint256 tokenId) external;

    /**
     * @notice Used to retrieve the address of the account approved to manage assets of a given token.
     * @dev Requirements:
     *
     *  - `tokenId` must exist.
     * @param tokenId ID of the token for which to retrieve the approved address
     * @return address Address of the account that is approved to manage the specified token's assets
     */
    function getApprovedForAssets(uint256 tokenId)
        external
        view
        returns (address);

    /**
     * @notice Used to add or remove an operator of assets for the caller.
     * @dev Operators can call {acceptAsset}, {rejectAsset}, {rejectAllAssets} or {setPriority} for any token
     *  owned by the caller.
     * @dev Requirements:
     *
     *  - The `operator` cannot be the caller.
     * @dev Emits an {ApprovalForAllForAssets} event.
     * @param operator Address of the account to which the operator role is granted or revoked from
     * @param approved The boolean value indicating whether the operator role is being granted (`true`) or revoked
     *  (`false`)
     */
    function setApprovalForAllForAssets(address operator, bool approved)
        external;

    /**
     * @notice Used to check whether the address has been granted the operator role by a given address or not.
     * @dev See {setApprovalForAllForAssets}.
     * @param owner Address of the account that we are checking for whether it has granted the operator role
     * @param operator Address of the account that we are checking whether it has the operator role or not
     * @return bool The boolean value indicating whether the account we are checking has been granted the operator role
     */
    function isApprovedForAllForAssets(address owner, address operator)
        external
        view
        returns (bool);
}
```
#### AssetSet

**概要**
アセットが初期化されたことを通知するイベント。

**パラメータ**
- `assetId`
    - 初期化されたアセットのID。

---

#### AssetAddedToTokens

**概要**
アセットがトークンの保留アセット配列に追加されたことを通知するイベント。

:::message
保留アセット配列は、**ERC5773**コンテキスト依存のマルチアセットトークンで使用される概念の1つです。
トークンの所有者や承認されたエンティティが、新しいアセットをトークンに追加する際に一時的に保持されるアセットのリストです。

具体的には、保留アセット配列に含まれるアセットは、まだトークンのアクティブアセットとして承認されていないアセットです。
トークンの所有者や承認されたエンティティが新しいアセットをトークンに追加する際、最初にこの保留アセット配列にアセットが追加されます。
その後、トークンの所有者や承認されたエンティティによる審査や承認プロセスを経て、アセットがアクティブアセットに移動されるか、あるいは拒否されるかが決まります。

保留アセット配列を介することで、アセットの追加や変更が確実に検証され、トークンの保護やトランザクションの透明性が確保されます。
この仕組みにより、アクティブアセットにアセットが移動される前に、トークン所有者や承認されたエンティティが対象アセットを審査することができるようになります。
:::

**パラメータ**
- `tokenIds`
    - 新しい保留アセットを受け取ったトークンのIDの配列。
- `assetId`
    - 追加されたアセットのID。
- `replacesId`
    - 置き換えられるアセットのID。

---

#### AssetAccepted

**概要**
アセットがトークンに受け入れられ、保留アセット配列からアクティブアセット配列に移動されたことを通知するイベント。

**パラメータ**
- `tokenId`
    - 新しいアセットが受け入れられたトークンのID。
- `assetId`
    - 受け入れられたアセットのID。
- `replacesId`
    - 置き換えられるアセットのID。

---

#### AssetRejected

**概要**
アセットがトークンから拒否され、保留アセット配列から削除されたことを通知するイベント。

**パラメータ**
- `tokenId`
    - アセットが拒否されたトークンのID。
- `assetId` 
    - 拒否されたアセットのID。

---

#### AssetPrioritySet

**概要**
トークンの優先アセット配列が再配置されたことを通知するイベント。

:::message
優先アセット配列は、**ERC5773**コンテキスト依存のマルチアセットトークンで使用される概念の1つです。
トークンの所有者や承認されたエンティティが、トークンに関連付けられたアクティブアセットの優先度を設定するための配列です。

具体的には、優先アセット配列は、アクティブアセットに関するアセットの優先度を示すために使用されます。
各アクティブアセットには、整数値（通常は`uint16`）で表される優先度が割り当てられます。
この優先度は、アクティブアセットの重要性や順序を示すために使用されます。

優先アセット配列は、トークンの所有者や承認されたエンティティがアクティブアセットの優先度を変更する際に使用されます。
トークンの所有者は、アクティブアセットの優先度を再構築するためにこの配列を設定することができます。
これにより、アセットの表示順や順序を調整することが可能になります。

優先アセット配列の利点は、トークンの所有者や承認されたエンティティが、アクティブアセットの表示や順序を制御することができることです。
これにより、特定のアクティブアセットを他のアクティブアセットよりも目立たせたり、重要なアセットを前面に配置したりすることができます。
:::

**パラメータ**
- `tokenId`
    - アセットの優先度配列が更新されたトークンのID。

---

#### ApprovalForAssets

**概要**
所有者がユーザーにトークンのアセット管理の承認を与えたことを通知するイベント。

**パラメータ**
- `owner` 
    - 承認を与えたアカウントのアドレス。
- `approved`
    - アセットの管理を許可されたアカウントのアドレス。
- `tokenId`
    - 承認が与えられたトークンのID。

---

もちろんです。以下に各イベントと関数の説明を提供します。

#### acceptAsset

**概要**
保留中のアセットを指定したトークンのアクティブアセットとして受け入れる関数。

**詳細**
保留中のアセットをトークンのアクティブアセットとして移行します。
アクティブアセットは、削除はされないが新しいアセットに置き換えることはできるものです。

**引数**
- `tokenId`
    - 受け入れ対象のトークンのID。
- `index`
    - 保留中のアセットの配列内でのインデックス。
- `assetId`
    - インデックスにあると期待されるアセットのID。

**戻り値**
なし

---

#### rejectAsset

**概要**
指定したトークンの保留中のアセットを拒否し、保留中のアセット配列から削除する関数。

**引数**
- `tokenId`
    - アセットが拒否されるトークンのID。
- `index`
    - 保留中のアセットの配列内でのインデックス。
- `assetId`
    - インデックスにあると期待されるアセットのID。

**戻り値**
なし

---

#### rejectAllAssets

**概要**
指定したトークンの保留中のアセットを全て拒否し、保留中のアセット配列を削除する関数。

**引数**
- `tokenId`
    - 保留中のアセットをクリアする対象のトークンのID。
- `maxRejections`
    - この操作の直前に到着したアセットが拒否されるのを防ぐための制限。

**戻り値**
なし

---

#### setPriority

**概要**
指定したトークンのアクティブアセットの優先度を設定する関数。

**詳細**
指定したトークンのアクティブアセットの優先度を設定します。
優先度配列は非連続な`uint16`のリストであり、値が低いほど高い優先度と見なされます。

**引数**
- `tokenId`
    - 優先度を設定する対象のトークンのID。
- `priorities`
    - アクティブアセットの優先度の配列。
    - 優先度配列の順序はアクティブアセット配列の順序と一致します。

**戻り値**
なし

---

#### getActiveAssets

**概要**
指定したトークンのアクティブアセットのIDの配列を取得する関数。

**詳細**
指定したトークンのアクティブアセットのIDの配列を取得します。
アクティブアセットのデータは参照によって保持され、IDに対応するデータにアクセスするために`getAssetMetadata`関数を呼び出すことができます。

**引数**
- `tokenId`
    - アクティブアセットのIDを取得する対象のトークンのID。

**戻り値**
- `uint64[] memory`
    - 指定したトークンのアクティブアセットのIDの配列。

---

もちろんです。以下に各イベントと関数の説明を提供します。

#### イベント・関数名: `getPendingAssets`

**概要**
指定したトークンの保留中のアセットのIDの配列を取得します。

**詳細**
この関数は、指定したトークンの保留中のアセットのIDの配列を取得します。アセットのデータは参照によって保持され、IDに対応するデータにアクセスするために`getAssetMetadata`関数を呼び出すことができます。

**引数**
- `tokenId`: 保留中のアセットのIDを取得する対象のトークンのID

**戻り値**
- `uint64[] memory`: 指定したトークンの保留中のアセットのIDの配列

---

#### イベント・関数名: `getActiveAssetPriorities`

**概要**
指定したトークンのアクティブアセットの優先度の配列を取得します。

**詳細**
この関数は、指定したトークンのアクティブアセットの優先度の配列を取得します。優先度配列は非連続な`uint16`の値の配列であり、アクティブアセットの優先度に対応します。

**引数**
- `tokenId`: アクティブアセットの優先度を取得する対象のトークンのID

**戻り値**
- `uint64[] memory`: 指定したトークンのアクティブアセットの優先度の配列

---

#### イベント・関数名: `getAssetReplacements`

**概要**
指定したトークンの保留中のアセットが受け入れられる場合に置き換えられるアセットのIDを取得します。

**詳細**
この関数は、指定したトークンの保留中のアセットが受け入れられる場合に、そのアセットが置き換えられるアセットのIDを取得します。アセットのデータは参照によって保持され、IDに対応するデータにアクセスするために`getAssetMetadata`関数を呼び出すことができます。

**引数**
- `tokenId`: 確認対象のトークンのID
- `newAssetId`: 受け入れられる保留中のアセットのID

**戻り値**
- `uint64`: 置き換えられるアセットのID

---

#### イベント・関数名: `getAssetMetadata`

**概要**
指定したトークンのアクティブアセットのうち、指定したインデックスにあるアセットのメタデータを取得します。

**詳細**
この関数は、指定したトークンのアクティブアセットのうち、指定したインデックスにあるアセットのメタデータを取得します。カスタムロジックを実装するためにオーバーライドすることもできます。

**引数**
- `tokenId`: アセットのメタデータを取得する対象のトークンのID
- `assetId`: アクティブアセットのIDで、対象のアセットが含まれることが期待されます

**戻り値**
- `string memory`: 指定したトークンのアクティブアセットのうち、指定したインデックスにあるアセットのメタデータ

---

#### イベント・関数名: `approveForAssets`

**概要**
ユーザーに対してトークンのアセットを管理する権限を付与します。

**詳細**
この関数は、ユーザーに対してトークンのアセットを管理する権限を付与します。転送承認とは異なり、アセットの受け入れや拒否、優先度の設定時には承認がクリアされません。ただし、トークンの転送時にはクリアされます。

**引数**
- `to`: 承認を付与するアカウントのアドレス
- `tokenId`: アセットの管理権限を付与するトークンのID

**戻り値**
なし

---

#### getApprovedForAssets

**概要**
指定したトークンのアセットの管理権限を持つアカウントのアドレスを取得する関数。

**引数**
- `tokenId`
    - 管理権限の対象となるトークンのID。

**戻り値**
- `address`
    - 指定したトークンのアセットの管理権限を持つアカウントのアドレス。

---

#### setApprovalForAllForAssets

**概要**
呼び出し元のアカウントのアセットのオペレーターを追加または削除する関数。

**詳細**
この関数は、呼び出し元のアカウントのアセットのオペレーターを追加または削除します。
オペレーターは、呼び出し元が所有する任意のトークンに対して`acceptAsset`、`rejectAsset`、`rejectAllAssets`、`setPriority`を呼び出すことができます。

**引数**
- `operator`
    - オペレーターの役割を付与または取り消すアカウントのアドレス。
- `approved`
    - オペレーターの役割を付与する（`true`）か取り消す（`false`）かを示す真偽値。

**戻り値**
なし

---

#### isApprovedForAllForAssets

**概要**
指定したアカウントが特定のアカウントからアセットのオペレーターロールを付与されているかどうかを確認する関数。

**引数**
- `owner`
    - オペレーターロールを付与したアカウントのアドレス。
- `operator`
    - オペレーターロールの有無を確認するアカウントのアドレス。

**戻り値**
- `bool`
    - 指定したアカウントが特定のアカウントからアセットのオペレーターロールを付与されているかどうかを示す真偽値。

---

`getAssetMetadata`関数は、アセットのメタデータURIを返します。
アセットのメタデータURIが指すメタデータには、以下のフィールドを持つJSONレスポンスを含めることができます。

```json
{
  "name": "Asset Name",
  "description": "The description of the token or asset",
  "mediaUri": "ipfs://mediaOfTheAssetOrToken",
  "thumbnailUri": "ipfs://thumbnailOfTheAssetOrToken",
  "externalUri": "https://uriToTheProjectWebsite",
  "license": "License name",
  "licenseUri": "https://uriToTheLicense",
  "tags": ["tags", "used", "to", "help", "marketplaces", "categorize", "the", "asset", "or", "token"],
  "preferThumb": false, // A boolean flag indicating to UIs to prefer thumbnailUri instead of mediaUri wherever applicable
  "attributes": [
    {
      "label": "rarity",
      "type": "string",
      "value": "epic",
      // For backward compatibility
      "trait_type": "rarity"
    },
    {
      "label": "color",
      "type": "string",
      "value": "red",
      // For backward compatibility
      "trait_type": "color"
    },
    {
      "label": "height",
      "type": "float",
      "value": 192.4,
      // For backward compatibility
      "trait_type": "height",
      "display_type": "number"
    }
  ]
}
```

これはアセットメタデータの推奨JSONスキーマですが、強制されるものではなく、実装者の好みに応じて全く異なる構造にしても問題ないです。

## 補足

#### トークンの構造を指す際に、Asset（資産）とResource（リソース）のどちらを使用すべきか？

最初は「**Multi-Resource（複数のリソース）**」という名前で考えられていましたが、この提案は1つのトークンが多様な構造を持つことを示しています。
そのため、「**Asset（資産）**」という用語が適しています。
資産はお金や不動産などの所有物を指し、この提案においてはマルチメディアファイル、技術情報、土地権利証明書など、実装者がトークンに組み込む資産の範囲を表現しています。

#### なぜEIP-712の許可スタイルの署名を使用しないのですか？

提案が既存の**ERC721**を拡張しているため、トークンの操作を承認する際には既に1つのトランザクションが使用されています。
この提案でメッセージの署名を追加することは一貫性を欠いてしまうため、**EIP712**スタイルの署名は採用されていません。

#### なぜインデックスを使用するのですか？

アセットのIDを使用して特定のアセットを検索する場合、配列を順に調べる必要があり、操作のコストはアクティブアセットや保留中のアセットの数に依存してしまいます。
インデックスを使用することで、操作のコストを一定に保つことができます。
各トークンごとにアクティブアセットと保留中のアセットのリストを維持する必要がありますが、それぞれのトークンのアクティブアセットと保留中のアセットはインデックスを使用して効率的にアクセスできるようになっています。
また、競合状態を避けるために、アセットを操作する際には対応するアセットIDも提供され、アセットの変更が確実に行われているか確認されます。

#### なぜ全てのアセットを取得するためのメソッドが含まれていないのですか？

全ての実装者にとって必要な操作でない可能性があるためです。
また、拡張として追加することや、フックを使用してシミュレートすることも可能です。

#### なぜページネーションが含まれていないのですか？

アセットIDは`uint64`を使用しており、読み取れるIDの上限は約`30,000`個です。
このため、一般的な使用ケースではページネーションが必要ないと判断されました。
ただし、必要な場合は実装者が独自にページネーション機能を追加することができます。

#### この提案は、同様の問題に対処しようとする他の提案とどのように異なるのですか？

この提案が他の同様の問題に対処しようとする提案と異なる点は以下の通りです。

##### 1. 単一のURIの使用
他の提案では、新しいアセットが必要になるたびに単一のURIが更新されるアプローチを採用しています。
しかし、これはトークンの所有者にとって信頼性の問題を引き起こす可能性があります。
つまり、URIが置き換えられる際に、所有者が意図しないアセットに切り替わる可能性があるという点です。

##### 2. 特定のアセットの種類への焦点
他の提案は特定のアセットの種類に焦点を当てており、その種類のアセットに限定的に適用されます。
しかし、この提案はアセットの種類に無関心であり、様々な種類のアセットに適用できるよう設計されています。

##### 3. 異なるユースケースごとに異なるトークンを使用
他の提案では、新しいユースケースごとに異なるトークンを作成して提供するアプローチが取られています。
しかし、これには将来の互換性の問題があります。
新たなユースケースが出現した場合、既存のトークンには対応できず、新たなトークンを作成する必要が生じます。

この提案は、上記の制限を解決するために次の点を重視しています。

- 信頼性の問題を解決するため、アセットの置き換えによるURIの切り替えではなく、アセットの追加や変更がトークン所有者の意図に従って行われる仕組みを提供します。
- 特定のアセットの種類に縛られず、さまざまな種類のアセットに柔軟に対応できる設計を行っています。
- 異なるユースケースに対応するために新たなトークンを作成するのではなく、同一のトークンを拡張することで将来の互換性を確保します。

### マルチアセットストレージスキーマ

この提案では、トークン内部にアセットを格納するために`uint64`の識別子の配列が使用されます。
アセットはそのアセットのデータを参照するための内部ストレージによって格納されます。
アセットエントリは`uint64`のマッピングを介してアセットデータに関連付けられたストレージに保存されます。
アセット配列は、これらの`uint64`のアセットIDの参照を格納する配列として機能します。
このような構造により、アセットの基本データは1回だけストレージに格納され、その後、そのアセットの参照がトークンコントラクト内に複数回追加できるようになります。
これによって、ストレージ上の冗長な文字列の使用が減少し、効率的なデータ管理が可能となります。
実装者は、アセット内の基本データとトークンIDに基づいて、コンテンツアドレスされたアーカイブへのリンクを手続き的に生成するために文字列の連結を使用できます。
アセットを新しいトークンに格納する際には、アセットごとに`16`バイトのストレージがアセット配列内に必要です。
この方法により、アセットの共有や再利用が効率的に行えるようになります。

:::message
この提案では、トークンが複数のアセットを保持するために、`uint64`の識別子の配列を使用します。
アセット自体のデータは、アセットの内容を実際に格納するのではなく、内部ストレージに関連づけられた`uint64`のマッピングを通じて格納されます。

例えば、仮想トークン（NFT）を考えてみましょう。
トークンは複数のアート作品や音楽ファイルなどのアセットを保持します。
各アセットはそれぞれ一意の`uint64`の識別子（**アセットID**）で識別されます。

アセット自体のデータは、内部ストレージに格納されるのではなく、アセットの内容を指すアセットIDが関連づけられます。
トークン内部には、アセットIDの配列が保持されます。
この配列の各要素は、実際のアセットを指すためのアセットIDを示します。

例えば、以下のような状況を考えてみましょう。

- トークンID: `1001`
- アセットID: `2001`, `2002`
- トークンID: `1002`
- アセットID: `2001`, `2003`

ここで、アセットID`2001`は2つのトークン（`1001`と`1002`）で共有されています。
しかし、アセットの実際のデータは、アセットID`2001`が指すものに一元的に格納されます。
このアプローチにより、同じアセットを複数のトークンで共有する際にデータの冗長性を排除し、効率的なデータ管理を実現します。

また、アセットを特定のトークンに関連付ける際に、アセットIDを使用して手続き的にURIを生成することも可能です。これにより、アセットの内容や種類に応じて、トークンごとに異なるリンクを生成できます。
:::

### アセットの追加のためのPropose-Commitパターン

既存のトークンに新しいアセットを追加する場合、提案とコミットの手順を組み合わせたプロセスを使用します。
まず、新しいアセットは「**保留中**」のアセット配列に追加されます。
その後、トークンの所有者によって「**保留中**」から「**アクティブ**」への移行が行われます。
このアプローチにより、アセットの追加がトークンの所有者によって制御されるだけでなく、スパムや不正行為の防止も行われます。
また、「**保留中**」のアセット配列はスロット数を制限することで、過剰なアセットの追加を防ぐ仕組みも取られます。

### アセットの管理

この提案には、アセットの管理を行うための複数の関数が含まれています。
アセットの「**保留中**」から「**アクティブ**」への移行を許可する機能の他に、トークンの所有者はアクティブおよび保留中のアセット配列からアセットを削除することもできます。
また、緊急時の対応策として、「**保留中**」のアセット配列内のすべてのエントリをクリアする機能も提供されます。
これにより、トークンの所有者はアセットの管理とトークンの健全性の維持を効果的に行うことができます。

## 後方互換性
マルチアセットトークン標準は、**ERC721**と互換性を持っています。

## テスト

以下にテストファイルを格納しています。

https://github.com/cardene777/base-code/blob/develop/contract/test/ERC5773/multiasset.ts

引用: https://eips.ethereum.org/EIPS/eip-5773

## 参考実装

以下にコードを格納しています。

https://github.com/cardene777/base-code/blob/develop/contract/contracts/ERC5773/MultiAssetToken.sol

引用: https://eips.ethereum.org/EIPS/eip-5773

## セキュリティ考慮事項

この提案において、**ERC721**と同じようなセキュリティの注意が必要です。
提案されている様々な関数、例えばトークンの削除、アセットの追加、アセットの承認などにおいて、予期しない隠れたロジックが存在する可能性があることに注意が必要です。
これらの関数は、意図せずにコントラクトの動作が変更されたり、不正な操作が行われることを防ぐために注意深く実装する必要があります。

また、特に注意が必要なのは監査されていないコントラクトとの取引です。
十分な監査が行われていないコントラクトは、セキュリティ上の脆弱性や問題を抱えている可能性があります。
そのため、これらのコントラクトとの取引には慎重に対処する必要があります。
不正な操作やデータの漏洩など、予期せぬリスクを避けるために、信頼性の高いコントラクトとの取引に優先的に注意を払うことが重要です。

## 引用

Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer), "ERC-5773: Context-Dependent Multi-Asset Tokens," Ethereum Improvement Proposals, no. 5773, October 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5773.

## 考察

複数のメディアやメタバースにおいて提供するアセットを切り替えることができるのは面白い規格だなと思いました。
**[ERC4955](/articles/149f522758cff5a8820a)** という、メタバースごとに提供する`tokenURI`を変更することができる規格と似ていると思ったのですが、今回提案されている規格ではコントラクト内でどのアセット（NFT）を提供するかを切り替えることができるので、むしろ組み合わせて使用したりできるのかなとも思いました。

[\[ERC4955\] 複数メタバースにNFTを適用させる仕組みを理解しよう！](/articles/149f522758cff5a8820a)

NFTの進化やデザインの変更などをユーザーに通知し、ユーザーごとにそれを受けいるか判断できるのは面白そうだなと思いました。
ただ、この場合、1つのNFTの中でデザインが変わるのではなく（`tokenURI`を切り替えるわけではない）、提供するNFT自体を変えているように見えるので、いろいろ課題が出てきそうです。
メタデータ自体を切り替えるような仕様にもできそうなので、自身で拡張するのか、はたまた私自身の勘違いの可能性もあるのでどこかで触ってみようと思います。

## 最後に

今回は「アクセスされる媒体によって提供するNFTを切り替えることで、NFTの提供形式を変えることができる提案である**ERC5773**」についてまとめてきました！
いかがだったでしょうか？

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

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