Alert Source Discuss
Standards Track: ERC

ERC-6220: 利用可装备部件的可组合NFT

通过固定和插槽部件装备,实现可组合的非同质化代币的接口。

Authors Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer)
Created 2022-12-20
Requires EIP-165, EIP-721, EIP-5773, EIP-6059

摘要

利用可装备部件的可组合NFT标准通过允许NFT选择性地通过装备向自身添加部件来扩展 ERC-721

可以通过为每个NFT实例从目录中选择部件列表来组合代币,并且能够将其他NFT装备到插槽中,这些插槽也在目录中定义。目录包含NFT可以从中组合的部件。

该提案引入了两种类型的部件:插槽类型的部件和固定类型的部件。插槽类型的部件允许将其他NFT集合装备到其中,而固定部件是具有其自身元数据的完整组件。

将部件装备到NFT中不会生成新代币,而是添加另一个组件,以便在检索代币时进行渲染。

动机

随着NFT成为以太坊生态系统中一种广泛的代币形式,并被用于各种用例,现在是时候标准化它们的额外效用了。代币能够装备其他代币并由一组可用部件组成的能力,可以提高实用性、可用性和向前兼容性。

ERC-721 发布以来的四年里,对额外功能的需求导致了无数的扩展。本EIP在以下方面改进了ERC-721:

组合

NFT可以协同工作以创建一个更大的结构。在本提案之前,多个NFT可以组合成一个单一的结构,要么通过检查与给定账户相关联的所有兼容NFT并随意使用(如果打算在同一插槽中使用多个NFT,可能会导致意外结果),要么通过保留部件的自定义分类帐以组合在一起(在智能合约或链下数据库中)。本提案建立了一个用于可组合NFT的标准化框架,其中单个NFT可以选择哪些部件应该成为整体的一部分,并且信息在链上。以这种方式组合NFT可以实现基础NFT的几乎无限的定制。这方面的一个例子可能是电影NFT。一些部分,如演职员表,应该是固定的。其他部分,如场景,应该是可互换的,以便可以替换各种版本(基础版本、扩展剪辑、周年纪念版…)。

代币进展

随着代币在其存在的各个阶段中进展,它可以获得或被授予各种部件。这可以用游戏来解释。角色可以用利用本提案的NFT来表示,并且能够装备通过游戏活动获得的装备,并且随着在游戏中进一步进展,将提供更好的物品。取代拥有大量NFT来表示通过其进展收集的物品,可以解锁可装备的部件,并且NFT所有者将能够决定装备哪些物品以及将哪些物品保存在库存中(未装备),而无需中间方。

优点追踪

可装备的NFT也可用于追踪优点。这方面的一个例子是学术优点。在这种情况下,可装备的NFT将代表一种数字学术成就投资组合,所有者可以装备他们的文凭、已发表的文章和奖项供所有人查看。

可证明的数字稀缺性

目前大多数NFT项目只是模拟稀缺。即使代币供应有限,这些代币的效用(如果有的话)也是没有上限的。例如,您可以使用同一个钱包和同一个NFT登录到同一游戏的500个不同的实例。然后,您可以同时将同一顶帽子装备到500个不同的游戏内头像上,因为它的视觉表示只是客户端机制。

该提案增加了强制执行的能力,如果一顶帽子装备在一个头像上(通过将其发送到该头像中然后装备),则不能将其装备在另一个头像上。这提供了真正的数字稀缺性。

规范

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

可装备的代币

可装备代币的核心智能合约的接口。

/// @title EIP-6220 利用可装备部件的可组合NFT
/// @dev 参见 https://eips.ethereum.org/EIPS/eip-6220
/// @dev 注意:此接口的ERC-165标识符为 0x28bc9ae4。

pragma solidity ^0.8.16;

import "./IERC5773.sol";

interface IERC6220 is IERC5773 /*, ERC165 */ {
    /**
     * @notice 用于存储`Equippable`组件的核心结构。
     * @return assetId 装备子项的资产的ID
     * @return childAssetId 用作装备的资产的ID
     * @return childId 已装备的代币的ID
     * @return childEquippableAddress 子资产所属的集合的地址
     */
    struct Equipment {
        uint64 assetId;
        uint64 childAssetId;
        uint256 childId;
        address childEquippableAddress;
    }

    /**
     * @notice 用于提供一个用于输入装备数据的结构。
     * @dev 仅用于输入,不用于存储数据。
     * @return tokenId 我们正在管理的代币的ID
     * @return childIndex 代币的活动子项列表中的子项索引
     * @return assetId 我们正在装备到的资产的ID
     * @return slotPartId 我们用于装备的插槽部件的ID
     * @return childAssetId 我们正在装备的资产的ID
     */
    struct IntakeEquip {
        uint256 tokenId;
        uint256 childIndex;
        uint64 assetId;
        uint64 slotPartId;
        uint64 childAssetId;
    }

    /**
     * @notice 用于通知侦听器,子项的资产已装备到其某个父资产中。
     * @param tokenId 装备了资产的代币的ID
     * @param assetId 与我们正在装备到的代币相关联的资产的ID
     * @param slotPartId 我们用于装备的插槽的ID
     * @param childId 正在装备到插槽中的子代币的ID
     * @param childAddress 子代币集合的地址
     * @param childAssetId 与我们正在装备的代币相关联的资产的ID
     */
    event ChildAssetEquipped(
        uint256 indexed tokenId,
        uint64 indexed assetId,
        uint64 indexed slotPartId,
        uint256 childId,
        address childAddress,
        uint64 childAssetId
    );

    /**
     * @notice 用于通知侦听器,子项的资产已从其某个父资产中解除装备。
     * @param tokenId 解除了资产装备的代币的ID
     * @param assetId 与我们正在解除装备的代币相关联的资产的ID
     * @param slotPartId 我们正在解除装备的插槽的ID
     * @param childId 正在解除装备的代币的ID
     * @param childAddress 正在解除装备的代币所属的集合的地址
     * @param childAssetId 与我们正在解除装备的代币相关联的资产的ID
     */
    event ChildAssetUnequipped(
        uint256 indexed tokenId,
        uint64 indexed assetId,
        uint64 indexed slotPartId,
        uint256 childId,
        address childAddress,
        uint64 childAssetId
    );

    /**
     * @notice 用于通知侦听器,属于`equippableGroupId`的资产已被标记为可装备到给定的插槽和父项中
     * @param equippableGroupId 被标记为可装备到与`parentAddress`集合的`slotPartId`相关联的插槽中的可装备组的ID
     * @param slotPartId 目录的插槽部件的ID,属于与`equippableGroupId`相关联的可装备组的部件可以装备到该目录中
     * @param parentAddress 集合的地址,属于`equippableGroupId`的部件可以装备到该集合中
     */
    event ValidParentEquippableGroupIdSet(
        uint64 indexed equippableGroupId,
        uint64 indexed slotPartId,
        address parentAddress
    );

    /**
     * @notice 用于将子项装备到代币中。
     * @dev `IntakeEquip`结构包含以下数据:
     *  [
     *      tokenId,
     *      childIndex,
     *      assetId,
     *      slotPartId,
     *      childAssetId
     *  ]
     * @param data 指定装备数据的`IntakeEquip`结构
     */
    function equip(
        IntakeEquip memory data
    ) external;

    /**
     * @notice 用于从父代币中解除装备子项。
     * @dev 只能由代币的所有者或已被当前所有者授予权限以管理给定代币的帐户调用。
     * @param tokenId 从中解除装备子项的父代的ID
     * @param assetId 包含子项已装备到的`Slot`的父代的资产的ID
     * @param slotPartId 从中解除装备子项的`Slot`的ID
     */
    function unequip(
        uint256 tokenId,
        uint64 assetId,
        uint64 slotPartId
    ) external;

    /**
     * @notice 用于检查代币是否装备了给定的子项。
     * @dev 用于防止传输已装备的子项。
     * @param tokenId 我们要查询的父代币的ID
     * @param childAddress 子代币智能合约的地址
     * @param childId 子代币的ID
     * @return bool 指示子代币是否已装备到给定代币中的布尔值
     */
    function isChildEquipped(
        uint256 tokenId,
        address childAddress,
        uint256 childId
    ) external view returns (bool);

    /**
     * @notice 用于验证代币是否可以装备到给定父代的插槽中。
     * @param parent 父代币智能合约的地址
     * @param tokenId 我们要装备的代币的ID
     * @param assetId 与我们要装备的代币相关联的资产的ID
     * @param slotId 我们要将代币装备到的插槽的ID
     * @return bool 指示具有给定资产的代币是否可以装备到所需插槽中的布尔值
     */
    function canTokenBeEquippedWithAssetIntoSlot(
        address parent,
        uint256 tokenId,
        uint64 assetId,
        uint64 slotId
    ) external view returns (bool);

    /**
     * @notice 用于获取装备到所需代币的指定插槽中的Equipment对象。
     * @dev `Equipment`结构包含以下数据:
     *  [
     *      assetId,
     *      childAssetId,
     *      childId,
     *      childEquippableAddress
     *  ]
     * @param tokenId 我们要检索其装备的对象的代币的ID
     * @param targetCatalogAddress 与代币的`Slot`部件相关联的`Catalog`的地址
     * @param slotPartId 我们正在检查已装备对象的`Slot`部件的ID
     * @return struct 包含有关已装备对象的数据的`Equipment`结构
     */
    function getEquipment(
        uint256 tokenId,
        address targetCatalogAddress,
        uint64 slotPartId
    ) external view returns (Equipment memory);

    /**
     * @notice 用于获取与给定`assetId`相关联的资产和可装备数据。
     * @param tokenId 要检索资产的代币的ID
     * @param assetId 我们要检索的资产的ID
     * @return metadataURI 资产的元数据URI
     * @return equippableGroupId 此资产所属的可装备组的ID
     * @return catalogAddress 部件所属的目录的地址
     * @return partIds 资产中包含的部件的ID数组
     */
    function getAssetAndEquippableData(uint256 tokenId, uint64 assetId)
        external
        view
        returns (
            string memory metadataURI,
            uint64 equippableGroupId,
            address catalogAddress,
            uint64[] calldata partIds
        );
}

目录

包含可装备部件的目录的接口。目录是可装备的固定和插槽部件的集合,不限于单个集合,但可以支持任意数量的NFT集合。

/**
 * @title ICatalog
 * @notice 可装备模块的目录接口。
 * @dev 注意:此接口的ERC-165标识符为 0xd912401f。
 */

pragma solidity ^0.8.16;

interface ICatalog /* is IERC165 */ {
    /**
     * @notice 用于宣布添加新部件的事件。
     * @dev 在添加新部件时发出。
     * @param partId 添加的部件的ID
     * @param itemType 用于指定部件是`None`、`Slot`还是`Fixed`的枚举值
     * @param zIndex 一个指定部件的z值的uint。用于指定应在哪个深度渲染部件
     * @param equippableAddresses 可以装备此部件的地址数组
     * @param metadataURI 部件的元数据URI
     */
    event AddedPart(
        uint64 indexed partId,
        ItemType indexed itemType,
        uint8 zIndex,
        address[] equippableAddresses,
        string metadataURI
    );

    /**
     * @notice 用于宣布向部件添加新的可装备项的事件。
     * @dev 当新的地址被标记为`partId`的可装备项时发出。
     * @param partId 添加了新的可装备地址的部件的ID
     * @param equippableAddresses 可以装备此部件的新地址数组
     */
    event AddedEquippables(
        uint64 indexed partId,
        address[] equippableAddresses
    );

    /**
     * @notice 用于宣布覆盖部件的可装备地址的事件。
     * @dev 当标记为`partId`的可装备项的现有地址列表被新的地址列表覆盖时发出。
     * @param partId 其可装备地址列表被覆盖的部件的ID
     * @param equippableAddresses 可以装备此部件的新的完整地址列表
     */
    event SetEquippables(uint64 indexed partId, address[] equippableAddresses);

    /**
     * @notice 用于宣布任何地址都可以装备给定的部件的事件。
     * @dev 当给定的部件被标记为任何地址都可以装备时发出。
     * @param partId 被标记为任何地址都可以装备的部件的ID
     */
    event SetEquippableToAll(uint64 indexed partId);

    /**
     * @notice 用于定义项目的类型。可能的值为`None`、`Slot`或`Fixed`。
     * @dev 用于固定和插槽部件。
     */
    enum ItemType {
        None,
        Slot,
        Fixed
    }

    /**
     * @notice 定义它的标准RMRK目录项的完整结构。
     * @dev 每个目录项至少需要3个存储槽,相当于柏林硬分叉(2021年4月14日)时约60,000个gas,但考虑到IPFS URI的标准长度,5-7个存储槽更现实。这将导致每250个资产消耗25,000,000到35,000,000个gas--以太坊主网的最大区块大小在高峰使用时为30M。
     * @return itemType 部件的项目类型
     * @return z 部件的z值,定义了在展示完整的NFT时应如何渲染它
     * @return equippable 允许在此部件中装备的地址数组
     * @return metadataURI 部件的元数据URI
     */
    struct Part {
        ItemType itemType; //1字节
        uint8 z; //1字节
        address[] equippable; //n 可以装备到此插槽中的集合
        string metadataURI; //n 字节 32+
    }

    /**
     * @notice 用于添加新的`Part`的结构。
     * @dev 使用指定的ID添加部件,因此您必须确保您使用的是未使用的`partId`,否则将恢复添加部件的操作。
     * @dev 完整的`IntakeStruct`如下所示:
     *  [
     *          partID,
     *      [
     *          itemType,
     *          z,
     *          [
     *               permittedCollectionAddress0,
     *               permittedCollectionAddress1,
     *               permittedCollectionAddress2
     *           ],
     *           metadataURI
     *       ]
     *   ]
     * @return partId 要分配给`Part`的ID
     * @return part 要添加的`Part`
     */
    struct IntakeStruct {
        uint64 partId;
        Part part;
    }

    /**
     * @notice 用于返回与目录关联的元数据URI。
     * @return string 基本元数据URI
     */
    function getMetadataURI() external view returns (string memory);

    /**
     * @notice 用于返回与目录关联的`itemType`
     * @return string 与目录关联的`itemType`
     */
    function getType() external view returns (string memory);

    /**
     * @notice 用于检查是否允许给定的地址装备所需的`Part`。
     * @dev 如果集合可以装备具有`partId`的资产,则返回true。
     * @param partId 我们要检查的部件的ID
     * @param targetAddress 我们要检查是否可以将部件装备到其中的地址
     * @return bool 指示是否可以将`targetAddress`装备到具有`partId`的`Part`中的状态
     */
    function checkIsEquippable(uint64 partId, address targetAddress)
        external
        view
        returns (bool);

    /**
     * @notice 用于检查所有地址是否都可以装备该部件。
     * @dev 如果该部件可以装备给所有人,则返回true。
     * @param partId 我们要检查的部件的ID
     * @return bool 指示是否可以由任何地址装备具有`partId`的部件的状态
     */
    function checkIsEquippableToAll(uint64 partId) external view returns (bool);

    /**
     * @notice 用于检索ID为`partId`的`Part`
     * @param partId 我们要检索的部件的ID
     * @return struct 与给定的`partId`关联的`Part`结构
     */
    function getPart(uint64 partId) external view returns (Part memory);

    /**
     * @notice 用于同时检索多个部件。
     * @param partIds 我们要检索的部件ID数组
     * @return struct 与给定的`partIds`关联的`Part`结构数组
     */
    function getParts(uint64[] calldata partIds)
        external
        view
        returns (Part[] memory);
}

理由

在设计提案时,我们考虑了以下问题:

  1. 为什么我们使用目录而不是支持直接NFT装备? 如果NFT可以直接装备到其他NFT中而没有任何监督,则生成的组合将是不可预测的。目录允许预先验证部件,以便生成按预期组合的组合。目录的另一个好处是能够定义可重用的固定部件。
  2. 为什么我们提出两种类型的部件? 对于所有代币都相同的一些部件,没有必要用单独的NFT来表示,因此可以用固定部件来表示。这减少了所有者钱包的混乱,并引入了一种有效的分发与NFT相关的重复资产的方式。 插槽部件允许将NFT装备到其中。这提供了在验证不相关的集合以正确组合后,将不相关的NFT集合装备到基本NFT中的能力。 拥有两个部件可以支持许多用例,并且由于该提案不强制使用这两个部件,因此可以将其应用于所需的任何配置中。
  3. 为什么不包括获取所有已装备部件的方法? 获取所有部件可能不是所有实施者都必需的操作。此外,它可以作为扩展添加,可以通过钩子实现,也可以使用索引器模拟。
  4. 目录应该限制为一次支持一个NFT集合,还是能够支持任意数量的集合? 由于目录的设计方式与使用它的用例无关。尽可能广泛地支持重用是有意义的。拥有一个目录支持多个集合可以优化操作,并降低部署和设置固定和插槽部件时的gas价格。

固定部件

固定部件在目录中定义和包含。它们有自己的元数据,并不意味着在NFT的生命周期中发生更改。

固定部件不能被替换。

固定部件的好处在于,它们代表可以由任意数量的集合中的任意数量的代币装备的可装备部件,并且只需要定义一次。

插槽部件

插槽部件在目录中定义和包含。它们没有自己的元数据,而是支持将选定的NFT集合装备到其中。但是,装备到插槽中的代币包含它们自己的元数据。这允许基本NFT的可装备的可修改内容由其所有者控制。由于它们可以装备到任意数量的集合中的任意数量的代币中,因此它们允许通过验证给定的插槽可以装备哪些NFT一次然后重复使用任意次数来可靠地组合最终代币。

向后兼容性

可装备代币标准已经与 ERC-721 兼容,以便利用可用于ERC-721实施的强大的工具,并确保与现有的ERC-721基础架构兼容。

测试用例

测试包含在 equippableFixedParts.tsequippableSlotParts.ts 中。

要在终端中运行它们,您可以使用以下命令:

cd ../assets/eip-6220
npm install
npx hardhat test

参考实现

请参阅 EquippableToken.sol

安全考虑

ERC-721 相同的安全注意事项适用:隐藏的逻辑可能存在于任何函数中,包括burn、add resource、accept resource等等。

建议在处理未经审计的合约时要谨慎。

版权

通过 CC0 放弃版权和相关权利。

Citation

Please cite this document as:

Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer), "ERC-6220: 利用可装备部件的可组合NFT," Ethereum Improvement Proposals, no. 6220, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6220.