ERC-5773: 上下文相关的多资产代币
用于多资产代币的接口,该接口具有由所有者偏好控制的上下文相关的资产类型输出。
Authors | Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) |
---|---|
Created | 2022-10-10 |
Requires | EIP-165, EIP-721 |
Table of Contents
摘要
多资产 NFT 标准允许构建一个新的原语:每个 NFT 的上下文相关的信息输出。
上下文相关的信息输出意味着以适当格式显示的资产是基于访问代币的方式。例如,如果在电子书阅读器中打开代币,则显示 PDF 资产;如果在市场中打开代币,则显示 PNG 或 SVG 资产;如果从游戏中访问代币,则访问 3D 模型资产;如果 (物联网) IoT 集线器访问代币,则访问提供必要寻址和规范信息的资产。
一个 NFT 可以有多个资产(输出),这些资产可以是提供给消费者的任何类型的文件,并按优先级对其进行排序。它们的 mimetype 或 tokenURI 不需要匹配,也不相互依赖。资产不是独立的实体,而应被视为“命名空间的 tokenURIs”,所有者可以随意对其进行排序,但只有在代币所有者和代币发行者都同意的情况下才能修改、更新、添加或删除。
动机
随着 NFT 成为以太坊生态系统中的一种广泛的代币形式,并被用于各种用例,现在是时候为它们标准化额外的效用了。将多个资产与单个 NFT 相关联可以提高实用性、可用性和前向兼容性。
自 ERC-721 发布以来的四年中,对额外功能的需求导致了无数的扩展。此 EIP 在以下领域改进了 ERC-721:
跨元宇宙兼容性
在撰写本提案时,元宇宙仍然是一个新生事物,尚未完全定义。无论元宇宙的定义如何演变,该提案都可以支持任意数量的不同实现。
跨元宇宙兼容性也可以称为跨引擎兼容性。一个例子是游戏 A 的化妆品在游戏 B 中不可用,因为框架不兼容。
可以通过新的附加资产为这种 NFT 提供进一步的效用:更多的游戏,更多的化妆品,附加到同一个 NFT。因此,作为 NFT 的游戏化妆品成为一个不断发展的具有无限效用的 NFT。
以下是一个更具体的例子。一种资产是游戏 A 的化妆品,一个包含化妆品资产的文件。另一种是游戏 B 的化妆品资产文件。第三种是旨在在目录、市场、投资组合跟踪器或其他通用 NFT 查看器中显示的通用资产,其中包含化妆品的表示、风格化缩略图和动画演示/预告片。
此 EIP 添加了一个抽象层,允许游戏开发者直接从用户的 NFT 中提取资产数据,而不是对其进行硬编码。
多媒体输出
电子书的 NFT 可以表示为 PDF、MP3 或其他格式,具体取决于加载它的软件。如果加载到电子书阅读器中,则应显示 PDF,如果加载到有声读物应用程序中,则应使用 MP3 表示形式。其他元数据可能存在于 NFT 中(可能是书籍的封面图像),用于在各种市场、搜索引擎结果页面 (SERP) 或投资组合跟踪器上进行识别。
媒体冗余
许多 NFT 在铸造时没有考虑到最佳实践 - 特别是,许多 NFT 在铸造时元数据集中在某处的服务器上,或者在某些情况下,硬编码的 IPFS 网关也可能关闭,而不是仅仅使用 IPFS 哈希。
通过将相同的元数据文件添加为不同的资产,例如,Arweave 上的元数据及其链接图像的一个资产,Sia 上的相同组合的一个资产,IPFS 上的相同组合的另一个资产等,元数据及其引用信息的弹性呈指数级增长,因为所有协议同时关闭的可能性变得越来越小。
NFT 进化
许多 NFT,特别是与游戏相关的 NFT,需要进化。在现代元宇宙中尤其如此,因为没有元宇宙实际上是元宇宙 - 它只是托管在某人服务器上的多人游戏,该服务器用读取帐户的 NFT 余额来代替用户名/密码登录。
当服务器关闭或游戏关闭时,玩家最终什么也得不到(经验损失)或得到一些不相关的(与游戏体验无关的资产或配件,垃圾邮件钱包,与其他“verses”不兼容 - 请参阅上面的跨元宇宙兼容性)。
使用多资产 NFT,铸币者或另一个预先批准的实体可以向 NFT 所有者建议一个新资产,NFT 所有者可以接受或拒绝它。该资产甚至可以定位要替换的现有资产。
替换资产在某种程度上类似于替换 ERC-721 代币的 URI。当资产被替换时,会保留一条清晰的可追溯性;旧资产仍然可以访问和验证。替换资产的元数据 URI 会模糊此谱系。如果发行人不能随意替换 NFT 的资产,它也会给代币所有者更多的信任。此提案的建议-接受资产替换机制提供了这种保证。
这允许升级机制,一旦收集到足够的经验,用户就可以接受升级。升级包括将新资产添加到 NFT,一旦接受,此新资产将替换旧资产。
举一个具体的例子,想想 Pokemon™️ 的进化 - 一旦获得足够的经验,训练师就可以选择进化他们的怪物。使用多资产 NFT,无需集中控制元数据来替换它,也无需将另一个 NFT 空投到用户的钱包中 - 相反,新的 Raichu 资产被铸造到 Pikachu 上,如果被接受,Pikachu 资产就会消失,被 Raichu 替换,Raichu 现在有自己的属性、值等。
另一种例子可能是 IoT 设备的固件的版本控制。资产可以代表其当前的固件,一旦更新可用,当前的资产可以用包含更新固件的资产替换。
规范
本文档中的关键词“必须”、“禁止”、“必需”、“应该”、“不应该”、“推荐”、“可以”和“可选”应解释为 RFC 2119 中所述。
/// @title ERC-5773 上下文相关的多资产代币
/// @dev 请参阅 https://eips.ethereum.org/EIPS/eip-5773
/// @dev 注意:此接口的 ERC-165 标识符为 0x06b4329a。
pragma solidity ^0.8.16;
interface IERC5773 /* is ERC165 */ {
/**
* @notice 用于通知监听器在 `assetId` 处初始化了一个资产对象。
* @param assetId 已初始化的资产的 ID
*/
event AssetSet(uint64 assetId);
/**
* @notice 用于通知监听器 `assetId` 处的资产对象已添加到代币的待处理资产数组中。
* @param tokenIds 接收到新待处理资产的代币的 ID 数组
* @param assetId 已添加到代币的待处理资产数组的资产的 ID
* @param replacesId 将被替换的资产的 ID
*/
event AssetAddedToTokens(
uint256[] tokenIds,
uint64 indexed assetId,
uint64 indexed replacesId
);
/**
* @notice 用于通知监听器 `assetId` 处的资产对象已被代币接受,并从代币的待处理资产数组迁移到代币的活动资产数组。
* @param tokenId 接受了新资产的代币的 ID
* @param assetId 已接受的资产的 ID
* @param replacesId 已替换的资产的 ID
*/
event AssetAccepted(
uint256 indexed tokenId,
uint64 indexed assetId,
uint64 indexed replacesId
);
/**
* @notice 用于通知监听器 `assetId` 处的资产对象已从代币中拒绝,并从代币的待处理资产数组中删除。
* @param tokenId 拒绝了资产的代币的 ID
* @param assetId 已拒绝的资产的 ID
*/
event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId);
/**
* @notice 用于通知监听器代币的优先级数组已重新排序。
* @param tokenId 更新了资产优先级数组的代币的 ID
*/
event AssetPrioritySet(uint256 indexed tokenId);
/**
* @notice 用于通知监听器所有者已授予用户管理给定代币的资产的权限。
* @dev 转移时必须清除批准
* @param owner 已授予所有代币资产批准的帐户的地址
* @param approved 已被授予管理代币资产的批准的帐户的地址
* @param tokenId 授予批准的代币的 ID
*/
event ApprovalForAssets(
address indexed owner,
address indexed approved,
uint256 indexed tokenId
);
/**
* @notice 用于通知监听器所有者已授予用户管理其所有代币的资产的权限。
* @param owner 已授予管理其所有代币上所有资产的批准的帐户的地址
* @param operator 已被授予管理所有代币的代币资产的批准的帐户的地址
* @param approved 一个布尔值,表示是否已授予 (`true`) 或撤销 (`false`) 权限
*/
event ApprovalForAllForAssets(
address indexed owner,
address indexed operator,
bool approved
);
/**
* @notice 接受给定代币的待处理数组中的资产。
* @dev 将资产从代币的待处理资产数组迁移到代币的活动资产数组。
* @dev 活动资产不能被任何人删除,但可以被新资产替换。
* @dev 要求:
*
* - 调用者必须拥有代币或被批准管理代币的资产
* - `tokenId` 必须存在。
* - `index` 必须在待处理资产数组长度的范围内。
* @dev 发出 {AssetAccepted} 事件。
* @param tokenId 要接受待处理资产的代币的 ID
* @param index 要接受的待处理数组中资产的索引
* @param assetId 预期在索引中的资产的 ID
*/
function acceptAsset(
uint256 tokenId,
uint256 index,
uint64 assetId
) external;
/**
* @notice 从给定代币的待处理数组中拒绝资产。
* @dev 从代币的待处理资产数组中删除资产。
* @dev 要求:
*
* - 调用者必须拥有代币或被批准管理代币的资产
* - `tokenId` 必须存在。
* - `index` 必须在待处理资产数组长度的范围内。
* @dev 发出 {AssetRejected} 事件。
* @param tokenId 资产被拒绝的代币的 ID
* @param index 要拒绝的待处理数组中资产的索引
* @param assetId 预期在索引中的资产的 ID
*/
function rejectAsset(
uint256 tokenId,
uint256 index,
uint64 assetId
) external;
/**
* @notice 拒绝来自给定代币的待处理数组的所有资产。
* @dev 有效地删除待处理数组。
* @dev 要求:
*
* - 调用者必须拥有代币或被批准管理代币的资产
* - `tokenId` 必须存在。
* @dev 发出 assetId = 0 的 {AssetRejected} 事件。
* @param tokenId 要清除其待处理数组的代币的 ID
* @param maxRejections 防止拒绝在此操作之前到达的资产。
*/
function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external;
/**
* @notice 为给定代币设置新的优先级数组。
* @dev 优先级数组是一个非顺序的 `uint16` 列表,其中最低的值被认为是最高优先级。
* @dev 优先级的值 `0` 是一种特殊情况,等同于未初始化。
* @dev 要求:
*
* - 调用者必须拥有代币或被批准管理代币的资产
* - `tokenId` 必须存在。
* - `priorities` 的长度必须等于活动资产数组的长度。
* @dev 发出 {AssetPrioritySet} 事件。
* @param tokenId 要设置优先级的代币的 ID
* @param priorities 活动资产的优先级数组。优先级数组中项目的顺序与活动数组中项目的顺序匹配
*/
function setPriority(uint256 tokenId, uint64[] calldata priorities)
external;
/**
* @notice 用于检索给定代币的活动资产的 ID。
* @dev 资产数据通过引用存储,为了访问与 ID 对应的数据,调用 `getAssetMetadata(tokenId, assetId)`。
* @dev 你可以安全地获得 1 万
* @param tokenId 要检索活动资产的 ID 的代币的 ID
* @return uint64[] 给定代币的活动资产 ID 的数组
*/
function getActiveAssets(uint256 tokenId)
external
view
returns (uint64[] memory);
/**
* @notice 用于检索给定代币的待处理资产的 ID。
* @dev 资产数据通过引用存储,为了访问与 ID 对应的数据,调用 `getAssetMetadata(tokenId, assetId)`。
* @param tokenId 要检索待处理资产的 ID 的代币的 ID
* @return uint64[] 给定代币的待处理资产 ID 的数组
*/
function getPendingAssets(uint256 tokenId)
external
view
returns (uint64[] memory);
/**
* @notice 用于检索给定代币的活动资产的优先级。
* @dev 资产优先级是一个非顺序的 uint16 值数组,数组大小等于活动资产优先级。
* @param tokenId 要检索活动资产的优先级的代币的 ID
* @return uint16[] 给定代币的活动资产的优先级数组
*/
function getActiveAssetPriorities(uint256 tokenId)
external
view
returns (uint64[] memory);
/**
* @notice 用于检索如果接受来自代币的待处理数组的给定资产将被替换的资产。
* @dev 资产数据通过引用存储,为了访问与 ID 对应的数据,调用 `getAssetMetadata(tokenId, assetId)`。
* @param tokenId 要检查的代币的 ID
* @param newAssetId 将被接受的待处理资产的 ID
* @return uint64 将被替换的资产的 ID
*/
function getAssetReplacements(uint256 tokenId, uint64 newAssetId)
external
view
returns (uint64);
/**
* @notice 用于获取指定代币的具有给定索引的活动资产的资产元数据。
* @dev 可以覆盖以实现枚举、回退或其他自定义逻辑。
* @param tokenId 从中检索资产元数据的代币的 ID
* @param assetId 资产 ID,必须在活动资产数组中
* @return string 属于代币的活动资产数组中指定索引的资产的元数据
*/
function getAssetMetadata(uint256 tokenId, uint64 assetId)
external
view
returns (string memory);
/**
* @notice 用于授予用户管理代币资产的权限。
* @dev 这与转移批准不同,因为当批准方接受或拒绝资产或设置资产优先级时,不会清除批准。此批准在代币转移时清除。
* @dev 一次只能批准一个帐户,因此批准 `0x0` 地址会清除之前的批准。
* @dev 要求:
*
* - 调用者必须拥有代币或是一个批准的 operator。
* - `tokenId` 必须存在。
* @dev 发出 {ApprovalForAssets} 事件。
* @param to 要授予批准的帐户的地址
* @param tokenId 授予管理资产批准的代币的 ID
*/
function approveForAssets(address to, uint256 tokenId) external;
/**
* @notice 用于检索被批准管理给定代币的资产的帐户的地址。
* @dev 要求:
*
* - `tokenId` 必须存在。
* @param tokenId 要检索批准地址的代币的 ID
* @return address 被批准管理指定代币的资产的帐户的地址
*/
function getApprovedForAssets(uint256 tokenId)
external
view
returns (address);
/**
* @notice 用于为调用者添加或删除资产的 operator。
* @dev Operators 可以为调用者拥有的任何代币调用 {acceptAsset}、{rejectAsset}、{rejectAllAssets} 或 {setPriority}。
* @dev 要求:
*
* - `operator` 不能是调用者。
* @dev 发出 {ApprovalForAllForAssets} 事件。
* @param operator 要向其授予或撤销 operator 角色的帐户的地址
* @param approved 指示是授予 (`true`) 还是撤销 (`false`) operator 角色的布尔值
*/
function setApprovalForAllForAssets(address operator, bool approved)
external;
/**
* @notice 用于检查地址是否已被给定地址授予 operator 角色。
* @dev 请参阅 {setApprovalForAllForAssets}。
* @param owner 我们正在检查是否已授予 operator 角色的帐户的地址
* @param operator 我们正在检查是否具有 operator 角色的帐户的地址
* @return bool 指示我们正在检查的帐户是否已被授予 operator 角色的布尔值
*/
function isApprovedForAllForAssets(address owner, address operator)
external
view
returns (bool);
}
getAssetMetadata
函数返回资产的元数据 URI。 资产的元数据 URI 指向的元数据 MAY 包含具有以下字段的 JSON 响应:
{
"name": "资产名称",
"description": "代币或资产的描述",
"mediaUri": "ipfs://mediaOfTheAssetOrToken",
"thumbnailUri": "ipfs://thumbnailOfTheAssetOrToken",
"externalUri": "https://uriToTheProjectWebsite",
"license": "许可证名称",
"licenseUri": "https://uriToTheLicense",
"tags": ["用于帮助市场对资产或代币进行分类的标签"],
"preferThumb": false, // 一个布尔标志,指示 UI 在适用的情况下首选 thumbnailUri 而不是 mediaUri
"attributes": [
{
"label": "稀有度",
"type": "string",
"value": "史诗",
// 为了向后兼容
"trait_type": "rarity"
},
{
"label": "颜色",
"type": "string",
"value": "红色",
// 为了向后兼容
"trait_type": "color"
},
{
"label": "高度",
"type": "float",
"value": 192.4,
// 为了向后兼容
"trait_type": "height",
"display_type": "number"
}
]
}
虽然这是资产元数据的建议 JSON 模式,但它不是强制性的,并且 MAY 基于实施者的偏好以完全不同的方式构建。
理由
在设计提案时,我们考虑了以下问题:
- 在引用构成代币的结构时,我们应该使用 Asset 还是 Resource?
最初的想法是将提案称为 Multi-Resource,但虽然这表示单个代币可以持有的结构的广泛性,但术语 asset 更好地代表了它。
资产被定义为由个人、公司或组织拥有的东西,例如金钱、财产或土地。这是对此提案的资产的最佳表示。 此提案中的资产可以是多媒体文件、技术信息、土地契约或实施者已决定作为其正在实施的代币的资产的任何内容。 - 为什么不使用 EIP-712 permit 风格的签名来管理批准?
为了保持一致性。 此提案扩展了 ERC-721,ERC-721 已经使用 1 个事务来批准使用代币的操作。 同时支持此操作和支持签名消息以进行资产操作将是不一致的。 - 为什么要使用索引?
为了减少 gas 消耗。 如果使用资产 ID 来查找要接受或拒绝哪个资产,则需要迭代数组,并且操作的成本将取决于活动或待处理资产数组的大小。 使用索引,成本是固定的。 每个代币都需要维护一个活动和待处理资产数组的列表,因为获取它们的方法是所提出的接口的一部分。
为了避免资产索引发生变化的竞争条件,在需要资产索引的操作中包含预期的资产 ID,以验证使用索引访问的资产是否为预期资产。
尝试了内部使用映射来跟踪索引的实现。 向代币添加资产的平均成本增加了 25% 以上,接受和拒绝资产的成本分别增加了 4.6% 和 7.1%。 我们得出结论,对于此提案来说这不是必需的,并且可以作为扩展来实现,以用于愿意接受此成本的用例。 在提供的示例实现中,有几个钩子使这成为可能。 - 为什么不包含获取所有资产的方法?
获取所有资产可能不是所有实施者都必需的操作。 此外,它可以作为扩展添加,可以使用钩子来完成,或者可以使用索引器来模拟。 - 为什么不包括分页?
资产 ID 使用uint64
,测试已确认在达到 gas 限制之前可以读取的 ID 限制约为 30.000。 这预计不是一个常见的用例,因此它不是接口的一部分。 但是,如果需要,实现者可以为此用例创建一个扩展。 - 此提案与其他尝试解决类似问题的提案有何不同?
在审查它们之后,我们得出结论,每个都包含以下至少一个限制:- 使用单个 URI,当需要新资产时会替换该 URI,这会给代币所有者带来信任问题。
- 仅关注一种资产类型,而此提案与资产类型无关。
- 每个新用例都有一个不同的代币,这意味着该代币不向前兼容。
多资产存储架构
资产作为 uint64
标识符的数组存储在代币中。
为了减少链上字符串的冗余存储,多资产代币通过内部存储按引用方式存储资产。 存储上的资产条目通过 uint64
映射到资产数据来存储。
资产数组是这些 uint64
资产 ID 引用的数组。
这样的结构允许将通用资产一次添加到存储中,并且可以根据需要多次将对其的引用添加到代币合约中。 然后,实施者可以使用字符串连接来程序化地生成到基于资产中的基本 SRC 和 token ID 的内容寻址归档文件的链接。 将资产存储在新代币中,每个代币对于重复和 tokenId
相关的资产仅占用资产数组中 16 字节的存储空间。
以这种方式构建代币的资产允许通过连接以编程方式派生 URI,尤其是在它们仅因 tokenId
而异时。
用于添加资产的提议-提交模式
向现有代币添加资产 MUST 以建议-提交模式的形式完成,以允许第三方有限的可变性。 向代币添加资产时,首先将其放置在 “Pending” 数组中,并且 MUST 由代币的所有者迁移到 “Active” 数组。 “Pending” 资产数组 SHOULD 限制为 128 个插槽,以防止垃圾邮件和恶意破坏。
资产管理
包括几个用于资产管理的函数。 除了从“待处理”到“活动”的许可迁移之外,代币的所有者 MAY 还可以从活动和待处理数组中删除资产 - 必须还包括一个紧急函数来清除待处理数组中的所有条目。
向后兼容性
多资产代币标准已与 ERC-721 兼容,以便利用可用于 ERC-721 实现的强大工具,并确保与现有 ERC-721 基础设施的兼容性。
测试用例
测试包含在 multiasset.ts
中。
要在终端中运行它们,可以使用以下命令:
cd ../assets/eip-5773
npm install
npx hardhat test
参考实现
请参阅 MultiAssetToken.sol
。
安全注意事项
与 ERC-721 相同的安全注意事项适用:隐藏逻辑可能存在于任何函数中,包括销毁、添加资产、接受资产等。
建议在处理未经审计的合约时要谨慎。
版权
通过 CC0 放弃版权和相关权利。
Citation
Please cite this document as:
Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer), "ERC-5773: 上下文相关的多资产代币," Ethereum Improvement Proposals, no. 5773, October 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5773.