[ERC5606] 1つのNFTを使用して複数プラットフォームで利用できるアセットを作成しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、1つのNFTを使用して複数のメタバース上やブロックチェーンゲーム上で使用できるアセットを作る**ERC5606**についてまとめていきます！

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

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


# 概要
この仕様では、ウェアラブルやゲーム内アイテムなどのデジタルアセットのためのマルチバースNFTを作成するための最小限の標準インターフェースを提案しています。

:::note info
「**そもそもインターフェースって何？**」という方は以下の記事を参考にしてください！
:::

https://chaldene.net/erc721#st-toc-h-5

この提案では、アセットが存在する各プラットフォーム上のデリゲートNFTをインデックス付けする（検索できるようにする）ことによって機能します。
利用できるプラットフォームはメタバースやPray to Earn Game、NFTマーケットプレイスのが挙げられます。
この提案は**ERC721**や**ERC1155**に依存して機能を拡張します。
この提案では、NFT保有者が個別に取引したりまとめて取引したりできるように、デリゲートNFTをマルチバースNFT内で「**バンドル化**」や「**分割**」することも可能です。

# 動機
**ERC721**や**ERC1155**のNFT規格で作られた、アバターウェアラブルや武器、盾、ポーションなどのゲーム内アイテムやメタバース内アセットを使用できる複数のプラットフォーム（「メタバース」や「ブロックチェーンゲーム」）が存在します。
上記の最大の欠点は、各プラットフォーム間の相互運用性がないことです。
そのため、同じデジタルアセット（アバターなど）を別々の**ERC721**や**ERC1155**のトークンとして異なるプラットフォーム上で公開する必要があります。
さらに、これらのトークン間には関連性がなく、現実では同じデジタルアセットを表していても、各アセットの希少性をオンチェーンで証明することは非常に難しいです。

NFTが実装されて以来、NFTは相互運用可能でデジタルアセットの希少性を証明できると言われてきました。
アイテムの希少性を証明できるが、相互運用性の側面はまだ課題が残っています。
異なるプラットフォーム間でデジタルアセットを所有し、相互運用性と真の所有権に向けた第一歩となるマルチバースNFT標準を作成することは重要なことです。

Web3エコシステムでは、NFTは複数の種類のユニークなアセットを表すために日々進化しています。
1つのアセットを所有することで、複数のプラットフォームで関連するNFTを所有している状態にします。
例えば、あるブランドが複数のメタバースで新しいスニーカーをリリースし、各プラットフォーム上で別個のNFTとして作成されるとします。
しかし、実際には全て同じスニーカーです。
メタバースやブロックチェーンゲームがより広まるにつれて、各プラットフォーム間のアセットの関係性と移動できることを伝える必要があります。
この問題に対処するためには、エコシステムにより良いフレームワークが必要です。
このフレームワークは、アセットの関係性と関連性の性質を定義する必要があります。


# 仕様

マルチバースNFTコントラクトは、複数のプラットフォーム上でデジタルアセットを扱えるようにします。
このコントラクトは、「**バンドル化**」や「**分割**」によって、複数プラットフォーム上のデジタルアセットのデリゲートNFTトークンを所有することができます。

```javascript
interface IMultiverseNFT {

   struct DelegateData {
       address contractAddress;
       uint256 tokenId;
       uint256 quantity;
   }

   event Bundled(uint256 multiverseTokenID, DelegateData[] delegateData, address ownerAddress);

   event Unbundled(uint256 multiverseTokenID, DelegateData[] delegateData);

   function delegateTokens(uint256 multiverseTokenID) external view returns (DelegateData[] memory);

   function unbundle(DelegateData[] memory delegateData, uint256 multiverseTokenID) external;

   function bundle(DelegateData[] memory delegateData, uint256 multiverseTokenID) external;

   function initBundle(DelegateData[] memory delegateData) external;
}
```

## DelegateData

デリゲートトークンの詳細を格納する構造体。

- `contractAddress`
    - デリゲートNFTのコントラクトアドレス。
- `tokenId`
    - デリゲートNFTのトークンID。
- `quantity`
    - デリゲートNFTの数量。

## Bundled

1つ以上の新しいデリゲートNFTがマルチバースNFTに追加された時に発行されるイベント。
- `multiverseTokenID`
    - マルチバースNFTのトークンID。
- `delegateData`
    - 追加されたデリゲートNFTの情報を含む配列。
- `ownerAddress`
    - 所有者のアドレス。


## Unbundled

1つ以上のデリゲートNFTがマルチバースNFTから削除された時に発行されるイベント。
- `multiverseTokenID`
    - マルチバースNFTのトークンID。
- `delegateData`
    - 削除されたデリゲートNFTの情報を含む配列。
- `ownerAddress`
    - 所有者のアドレス。


## delegateTokens

マルチバースNFTのトークンIDを受け取り、そのNFTに含まれるデリゲートトークンのデータの配列を返す関数。

## unbundle

マルチバースNFTから1つ以上のデリゲートNFTを削除する関数。
デリゲートNFTの情報を受け取り、NFTをマルチバースNFTコントラクトから所有者のアドレスに送付します。

## bundle

1つ以上のデリゲートNFTをマルチバースNFTに追加する関数。
デリゲートNFTの情報を受け取り、NFTをマルチバースNFTコントラクトに送付します。
デリゲートNFTに対してプログラムによる送付が可能になるように、このマルチバースNFTコントラクトに対して送付権限が与えられている必要があります。

## initBundle

新しいバンドルを初期化し、マルチバースNFTの作成と関数実行アドレスに送付権限を付与する関数。
新しいマルチバースNFTが初期化され他状態ではデリゲートNFTは含まれていません。
新しいマルチバースNFTのトークンIDを返します。

## 仕様の補足

この標準を実装するDAppは、`initBundle`関数を呼び出すことでバンドルを初期化します。
そうすると、新しいマルチバースNFTが作成され、関数実行者に送付権限が付与されます。
バンドルの作成時には、デリゲートトークンのコントラクトアドレスとトークンIDが初期化時に設定され、変更することはできません。
これにより、関連のないNFTが誤って一緒にバンドルされるという意図しない処理を回避できます。

バンドルが初期化されると、デリゲートNFTトークンは`bundle`関数を呼び出して、マルチバースNFTコントラクトに送付できます。
この際、マルチバースNFTのトークンIDを渡す必要があります。
DAppが`bundle`関数を呼び出す前に、デリゲートNFTを所有者からこのマルチバースNFTコントラクトに送付権限を付与する必要があります。
その後、マルチバースNFTは、さまざまなプラットフォーム上でこのデジタルアセットを1つ以上所有します。

マルチバースNFTの所有者が、個別のデリゲートNFTをいずれかのプラットフォームで個別に販売または使用したい場合は、`unbundle`関数を呼び出すことで実行できます。
この関数は、関数実行アドレスがマルチバースNFTの所有者である場合のみ、特定のデリゲートNFTトークンを関数実行アドレスに送付します。

# 補足
`delegateData`構造体には、各プラットフォーム上のデリゲートNFTトークンに関する情報が含まれています。`contractAddress`、`tokenId`、`quantity`を格納しNFTを区別します。
このNFTは、**ERC-721**や**ERC-1155**標準に従う必要があります。

`bundle`と`unbundle`関数は、部分的なバンドルおよびアンバンドルに対応する必要があるため、`DelegateData`構造体の配列を受け取ります。
例えば、ユーザーは3つのデリゲートNFTでバンドルを初期化することができますが、好きなタイミングで2つ以下のバンドルおよびアンバンドルを実行できる必要があります。
3つ以上のバンドルまたはアンバンドルはできません。
また、バンドルとアンバンドルを選択的に行うために、個々のトークンIDが必要です。

# 後方互換性
この標準は**ERC-721**や**ERC-1155**と完全に互換性があります。

# 参考実装

- `MultiverseNFT.sol`

```javascript
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract ERC721Full is ERC721Enumerable, ERC721URIStorage {
    /// @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
    /// @param name is a non-empty string
    /// @param symbol is a non-empty string
    constructor(string memory name, string memory symbol)
        ERC721(name, symbol)
    {}

    /// @dev Hook that is called before any token transfer. This includes minting and burning. `from`'s `tokenId` will be transferred to `to`
    /// @param from is an non-zero address
    /// @param to is an non-zero address
    /// @param tokenId is an uint256 which determine token transferred from `from` to `to`
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721Enumerable, ERC721) {
        ERC721Enumerable._beforeTokenTransfer(from, to, tokenId);
    }

    /// @notice Interface of the ERC165 standard
    /// @param interfaceId is a byte4 which determine interface used
    /// @return true if this contract implements the interface defined by `interfaceId`
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC721Enumerable, ERC721)
        returns (bool)
    {
        return
            ERC721.supportsInterface(interfaceId) ||
            ERC721Enumerable.supportsInterface(interfaceId);
    }

    /// @notice the Uniform Resource Identifier (URI) for `tokenId` token
    /// @param tokenId is unit256
    /// @return string of (URI) for `tokenId` token
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override(ERC721URIStorage, ERC721)
        returns (string memory)
    {
        return ERC721URIStorage.tokenURI(tokenId);
    }

    function _burn(uint256 tokenId)
        internal
        override(ERC721, ERC721URIStorage)
    {}
}

/**
 * @dev Interface of the Multiverse NFT standard as defined in the EIP.
 */
interface IMultiverseNFT {
    /**
     * @dev struct to store delegate token details
     *
     */
    struct DelegateData {
        address contractAddress;
        uint256 tokenId;
        uint256 quantity;
    }

    /**
     * @dev Emitted when one or more new delegate NFTs are added to a Multiverse NFT
     *
     */
    event Bundled(
        uint256 multiverseTokenID,
        DelegateData[] delegateData,
        address ownerAddress
    );

    /**
     * @dev Emitted when one or more delegate NFTs are removed from a Multiverse NFT
     */
    event Unbundled(uint256 multiverseTokenID, DelegateData[] delegateData);

    /**
     * @dev Accepts the tokenId of the Multiverse NFT and returns an array of delegate token data
     */
    function delegateTokens(uint256 multiverseTokenID)
        external
        view
        returns (DelegateData[] memory);

    /**
     * @dev Removes one or more delegate NFTs from a Multiverse NFT
     * This function accepts the delegate NFT details, and transfer those NFTs out of the Multiverse NFT contract to the owner's wallet
     */
    function unbundle(
        DelegateData[] memory delegateData,
        uint256 multiverseTokenID
    ) external;

    /**
     * @dev Adds one or more delegate NFTs to a Multiverse NFT
     * This function accepts the delegate NFT details, and transfers those NFTs to the Multiverse NFT contract
     * Need to ensure that approval is given to this Multiverse NFT contract for the delegate NFTs so that they can be transferred programmatically
     */
    function bundle(
        DelegateData[] memory delegateData,
        uint256 multiverseTokenID
    ) external;

    /**
     * @dev Initializes a new bundle, mints a Multiverse NFT and assigns it to msg.sender
     * Returns the token ID of a new Multiverse NFT
     * Note - When a new Multiverse NFT is initialized, it is empty, it does not contain any delegate NFTs
     */
    function initBundle(DelegateData[] memory delegateData) external;
}

abstract contract MultiverseNFT is
    IMultiverseNFT,
    Ownable,
    ERC721Full,
    IERC1155Receiver,
    AccessControl
{
    using SafeMath for uint256;
    bytes32 public constant BUNDLER_ROLE = keccak256("BUNDLER_ROLE");

    uint256 currentMultiverseTokenID;

    mapping(uint256 => DelegateData[]) public multiverseNFTDelegateData;
    mapping(uint256 => mapping(address => mapping(uint256 => uint256)))
        public tokenBalances;

    constructor(address bundlerAddress) {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(BUNDLER_ROLE, msg.sender);
        _setRoleAdmin(BUNDLER_ROLE, DEFAULT_ADMIN_ROLE);
        _setupRole(BUNDLER_ROLE, bundlerAddress);
    }

    function delegateTokens(uint256 multiverseTokenID)
        external
        view
        returns (DelegateData[] memory)
    {
        return multiverseNFTDelegateData[multiverseTokenID];
    }

    function initBundle(DelegateData[] memory delegateData) external {
        uint256 tokenId = currentMultiverseTokenID.add(1);
        for (uint256 i = 0; i < delegateData.length; i = i.add(1)) {
            bool isERC721 = _isERC721(delegateData[i].contractAddress);
            if (isERC721) {
                require(
                    delegateData[i].quantity == 1,
                    "ERC721 quantity must be 1"
                );
            }
            multiverseNFTDelegateData[tokenId].push(delegateData[i]);
        }

        _incrementMultiverseTokenID();
        _safeMint(msg.sender, tokenId);
    }

    function bundle(
        DelegateData[] memory delegateData,
        uint256 multiverseTokenID
    ) external {
        require(
            hasRole(BUNDLER_ROLE, msg.sender) ||
                ownerOf(multiverseTokenID) == msg.sender,
            "msg.sender neither have bundler role nor multiversetoken owner"
        );
        _bundle(delegateData, multiverseTokenID);
    }

    function unbundle(
        DelegateData[] memory delegateData,
        uint256 multiverseTokenID
    ) external {
        require(
            ownerOf(multiverseTokenID) == msg.sender,
            "msg.sender is not a multiversetoken owner"
        );
        for (uint256 i = 0; i < delegateData.length; i = i.add(1)) {
            require(
                _ensureDelegateBelongsToMultiverseNFT(
                    delegateData[i],
                    multiverseTokenID
                ),
                "delegate not assigned to multiverse token"
            );
            uint256 balance = tokenBalances[multiverseTokenID][
                delegateData[i].contractAddress
            ][delegateData[i].tokenId];
            require(
                delegateData[i].quantity <= balance,
                "quantity exceeds balance"
            );
            require(
                _ensureMultiverseContractOwnsDelegate(delegateData[i]),
                "delegate not owned by contract"
            );

            address contractAddress = delegateData[i].contractAddress;
            uint256 tokenId = delegateData[i].tokenId;
            uint256 quantity = delegateData[i].quantity;

            _updateDelegateBalances(delegateData[i], multiverseTokenID);

            if (_isERC721(contractAddress)) {
                ERC721Full erc721Instance = ERC721Full(contractAddress);
                erc721Instance.transferFrom(address(this), msg.sender, tokenId);
            } else if (_isERC1155(contractAddress)) {
                ERC1155Supply erc1155Instance = ERC1155Supply(contractAddress);
                erc1155Instance.safeTransferFrom(
                    address(this),
                    msg.sender,
                    tokenId,
                    quantity,
                    ""
                );
            }
        }
        emit Unbundled(multiverseTokenID, delegateData);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(AccessControl, ERC721Full, IERC165)
        returns (bool)
    {
        return
            AccessControl.supportsInterface(interfaceId) ||
            ERC721Full.supportsInterface(interfaceId);
    }

    function _bundle(
        DelegateData[] memory delegateData,
        uint256 multiverseTokenID
    ) internal {
        for (uint256 i = 0; i < delegateData.length; i = i.add(1)) {
            require(
                _ensureDelegateBelongsToMultiverseNFT(
                    delegateData[i],
                    multiverseTokenID
                ),
                "delegate not assigned to multiversetoken"
            );
            require(
                _ensureDelegateQuantityLimitForMMultiverseNFT(
                    delegateData[i],
                    multiverseTokenID
                ),
                "delegate quantity assigned to multiversetoken exceeds"
            );

            address contractAddress = delegateData[i].contractAddress;
            uint256 tokenId = delegateData[i].tokenId;
            uint256 quantity = delegateData[i].quantity;

            tokenBalances[multiverseTokenID][contractAddress][
                tokenId
            ] = tokenBalances[multiverseTokenID][contractAddress][tokenId].add(
                quantity
            );

            if (_isERC721(contractAddress)) {
                require(
                    quantity == 1,
                    "ERC721 cannot have quantity more than 1"
                );
                ERC721Full erc721Instance = ERC721Full(contractAddress);
                erc721Instance.transferFrom(msg.sender, address(this), tokenId);
            } else if (_isERC1155(contractAddress)) {
                ERC1155Supply erc1155Instance = ERC1155Supply(contractAddress);
                erc1155Instance.safeTransferFrom(
                    msg.sender,
                    address(this),
                    tokenId,
                    quantity,
                    ""
                );
            }
        }
        emit Bundled(
            multiverseTokenID,
            delegateData,
            ownerOf(multiverseTokenID)
        );
    }

    function _ensureDelegateBelongsToMultiverseNFT(
        DelegateData memory delegateData,
        uint256 multiverseTokenID
    ) internal view returns (bool) {
        DelegateData[] memory storedData = multiverseNFTDelegateData[
            multiverseTokenID
        ];
        for (uint256 i = 0; i < storedData.length; i = i.add(1)) {
            if (
                delegateData.contractAddress == storedData[i].contractAddress &&
                delegateData.tokenId == storedData[i].tokenId
            ) {
                return true;
            }
        }
        return false;
    }

    function _ensureMultiverseContractOwnsDelegate(
        DelegateData memory delegateData
    ) internal view returns (bool) {
        if (_isERC721(delegateData.contractAddress)) {
            ERC721Full erc721Instance = ERC721Full(
                delegateData.contractAddress
            );
            if (address(this) == erc721Instance.ownerOf(delegateData.tokenId)) {
                return true;
            }
        } else if (_isERC1155(delegateData.contractAddress)) {
            ERC1155Supply erc1155Instance = ERC1155Supply(
                delegateData.contractAddress
            );
            if (
                erc1155Instance.balanceOf(
                    address(this),
                    delegateData.tokenId
                ) >= delegateData.quantity
            ) {
                return true;
            }
        }
        return false;
    }

    function _ensureDelegateQuantityLimitForMMultiverseNFT(
        DelegateData memory delegateData,
        uint256 multiverseTokenID
    ) internal view returns (bool) {
        DelegateData[] memory storedData = multiverseNFTDelegateData[
            multiverseTokenID
        ];
        for (uint256 i = 0; i < storedData.length; i = i.add(1)) {
            if (
                delegateData.contractAddress == storedData[i].contractAddress &&
                delegateData.tokenId == storedData[i].tokenId
            ) {
                uint256 balance = tokenBalances[multiverseTokenID][
                    delegateData.contractAddress
                ][delegateData.tokenId];
                if (
                    balance.add(delegateData.quantity) <= storedData[i].quantity
                ) {
                    return true;
                }
                return false;
            }
        }
    }

    function _updateDelegateBalances(
        DelegateData memory delegateData,
        uint256 multiverseTokenID
    ) internal returns (uint256) {
        address contractAddress = delegateData.contractAddress;
        uint256 tokenId = delegateData.tokenId;
        tokenBalances[multiverseTokenID][contractAddress][
            tokenId
        ] = tokenBalances[multiverseTokenID][contractAddress][tokenId].sub(
            delegateData.quantity
        );
        return tokenBalances[multiverseTokenID][contractAddress][tokenId];
    }

    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    ) external pure override returns (bytes4) {
        return this.onERC1155Received.selector;
    }

    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external pure override returns (bytes4) {
        return this.onERC1155BatchReceived.selector;
    }

    function _isERC1155(address contractAddress) internal view returns (bool) {
        return IERC1155(contractAddress).supportsInterface(0xd9b67a26);
    }

    function _isERC721(address contractAddress) internal view returns (bool) {
        return IERC721(contractAddress).supportsInterface(0x80ac58cd);
    }

    function _incrementMultiverseTokenID() internal {
        currentMultiverseTokenID = currentMultiverseTokenID.add(1);
    }
}
```

# セキュリティ
`bundle`関数は外部のコントラクトを呼び出すため、リエントランシー攻撃対策を実装する必要があります。

:::note info
リエントランシー攻撃については以下を参考にしてください。
:::

https://chaldene.net/solidity-reentrancy-attack

# 最後に

今回は「1つのNFTを使用して複数のメタバース上やブロックチェーンゲーム上で使用できるアセットを作る**ERC5606**」についてまとめてきました！
いかがだったでしょうか？

実装については今後追記していきます。

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

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

# 採用強化中！

CryptoGamesでは一緒に働く仲間を大募集中です。

https://www.wantedly.com/companies/cryptogames

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。
「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう！