Alert Source Discuss
Standards Track: ERC

ERC-7432: 非同质化代币角色

用于 NFT 的角色管理。允许账户通过可过期的角色分配来分享 NFT 的效用。

Authors Ernani São Thiago (@ernanirst), Daniel Lima (@karacurt)
Created 2023-07-14
Requires EIP-165, EIP-721

摘要

本标准介绍了 NFT 的角色管理。每个角色分配都与单个 NFT 关联,并在给定的时间戳自动过期。角色被定义为 bytes32,并具有任意大小的自定义 data 字段,以允许自定义。

动机

NFT 角色接口旨在建立 NFT 中角色管理的标准。跟踪链上角色使去中心化应用程序 (dApp) 能够为特权操作实施访问控制,例如,使用角色铸造代币(空投声明权)。

NFT 角色可以与 dApp 深度集成,以创建效用共享机制。一个很好的例子是在数字房地产中。用户可以创建一个数字房产 NFT,并将 keccak256("PropertyManager()") 角色授予另一个用户,允许他们委派特定的效用,而不会损害所有权。同一用户还可以将 keccak256("PropertyTenant(uint256)") 角色授予其他用户,允许接收者访问数字房产并与之交互。

在去中心化金融 (DeFi) 中也有有趣的用例。保险单可以作为 NFT 发行,受益人、被保险人和保险人都可以是使用此标准跟踪的链上角色。

规范

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“NOT RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC-2119 和 RFC-8174 中的描述进行解释。

符合规范的合约必须实现以下接口:

/// @title ERC-7432 非同质化代币角色
/// @dev See https://eips.ethereum.org/EIPS/eip-7432
/// Note: the ERC-165 identifier for this interface is 0xd00ca5cf.
interface IERC7432 /* is ERC165 */ {
  struct Role {
    bytes32 roleId;
    address tokenAddress;
    uint256 tokenId;
    address recipient;
    uint64 expirationDate;
    bool revocable;
    bytes data;
  }

  /** Events **/

  /// @notice Emitted when an NFT is locked (deposited or frozen).
  /// @notice 当 NFT 被锁定(存入或冻结)时发出。
  /// @param _owner The owner of the NFT.
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  event TokenLocked(address indexed _owner, address indexed _tokenAddress, uint256 _tokenId);

  /// @notice Emitted when a role is granted.
  /// @notice 当角色被授予时发出。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  /// @param _owner The user assigning the role.
  /// @param _recipient The user receiving the role.
  /// @param _expirationDate The expiration date of the role.
  /// @param _revocable Whether the role is revocable or not.
  /// @param _data Any additional data about the role.
  event RoleGranted(
    address indexed _tokenAddress,
    uint256 indexed _tokenId,
    bytes32 indexed _roleId,
    address _owner,
    address _recipient,
    uint64 _expirationDate,
    bool _revocable,
    bytes _data
  );

  /// @notice Emitted when a role is revoked.
  /// @notice 当角色被撤销时发出。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  event RoleRevoked(address indexed _tokenAddress, uint256 indexed _tokenId, bytes32 indexed _roleId);

  /// @notice Emitted when an NFT is unlocked (withdrawn or unfrozen).
  /// @notice 当 NFT 被解锁(撤回或解冻)时发出。
  /// @param _owner The original owner of the NFT.
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  event TokenUnlocked(address indexed _owner, address indexed _tokenAddress, uint256 indexed _tokenId);

  /// @notice Emitted when a user is approved to manage roles on behalf of another user.
  /// @notice 当用户被批准代表另一个用户管理角色时发出。
  /// @param _tokenAddress The token address.
  /// @param _operator The user approved to grant and revoke roles.
  /// @param _isApproved The approval status.
  event RoleApprovalForAll(address indexed _tokenAddress, address indexed _operator, bool indexed _isApproved);

  /** External Functions **/

  /// @notice Grants a role to a user.
  /// @notice 授予用户角色。
  /// @dev Reverts if sender is not approved or the NFT owner.
  /// @dev 如果发送者未被批准或不是 NFT 所有者,则还原。
  /// @param _role The role attributes.
  function grantRole(Role calldata _role) external;

  /// @notice Revokes a role from a user.
  /// @notice 撤销用户的角色。
  /// @dev Reverts if sender is not approved or the original owner.
  /// @dev 如果发送者未被批准或不是原始所有者,则还原。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  function revokeRole(address _tokenAddress, uint256 _tokenId, bytes32 _roleId) external;

  /// @notice Unlocks NFT (transfer back to original owner or unfreeze it).
  /// @notice 解锁 NFT(转移回原始所有者或解冻它)。
  /// @dev Reverts if sender is not approved or the original owner.
  /// @dev 如果发送者未被批准或不是原始所有者,则还原。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  function unlockToken(address _tokenAddress, uint256 _tokenId) external;

  /// @notice Approves operator to grant and revoke roles on behalf of another user.
  /// @notice 批准操作员代表另一个用户授予和撤销角色。
  /// @param _tokenAddress The token address.
  /// @param _operator The user approved to grant and revoke roles.
  /// @param _approved The approval status.
  function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external;

  /** View Functions **/

  /// @notice Retrieves the original owner of the NFT.
  /// @notice 检索 NFT 的原始所有者。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @return owner_ The owner of the token.
  function ownerOf(address _tokenAddress, uint256 _tokenId) external view returns (address owner_);

  /// @notice Retrieves the recipient of an NFT role.
  /// @notice 检索 NFT 角色的接收者。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  /// @return recipient_ The user that received the role.
  function recipientOf(
    address _tokenAddress,
    uint256 _tokenId,
    bytes32 _roleId
  ) external view returns (address recipient_);

  /// @notice Retrieves the custom data of a role assignment.
  /// @notice 检索角色分配的自定义数据。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  /// @return data_ The custom data of the role.
  function roleData(
    address _tokenAddress,
    uint256 _tokenId,
    bytes32 _roleId
  ) external view returns (bytes memory data_);

  /// @notice Retrieves the expiration date of a role assignment.
  /// @notice 检索角色分配的到期日期。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  /// @return expirationDate_ The expiration date of the role.
  function roleExpirationDate(
    address _tokenAddress,
    uint256 _tokenId,
    bytes32 _roleId
  ) external view returns (uint64 expirationDate_);

  /// @notice Verifies whether the role is revocable.
  /// @notice 验证角色是否可撤销。
  /// @param _tokenAddress The token address.
  /// @param _tokenId The token identifier.
  /// @param _roleId The role identifier.
  /// @return revocable_ Whether the role is revocable.
  function isRoleRevocable(
    address _tokenAddress,
    uint256 _tokenId,
    bytes32 _roleId
  ) external view returns (bool revocable_);

  /// @notice Verifies if the owner approved the operator.
  /// @notice 验证所有者是否批准了操作员。
  /// @param _tokenAddress The token address.
  /// @param _owner The user that approved the operator.
  /// @param _operator The user that can grant and revoke roles.
  /// @return Whether the operator is approved.
  function isRoleApprovedForAll(
    address _tokenAddress,
    address _owner,
    address _operator
  ) external view returns (bool);
}

元数据扩展

Roles Metadata 扩展扩展了基于 JSON 的传统 NFT 元数据模式。因此,支持此功能的 dApp 还必须实现 ERC-721 的元数据扩展。此扩展是可选的,允许开发人员为角色提供其他信息。

更新后的元数据模式:

{
  
  /** Existing NFT Metadata **/
  /** 现有的 NFT 元数据 **/

  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset to which this NFT represents"
      // "description": "标识此 NFT 代表的资产"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset to which this NFT represents"
      // "description": "描述此 NFT 代表的资产"
    },
    "image": {
      "type": "string",
      "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive"
      // "description": "指向具有 mime 类型 image/* 的资源的 URI,表示此 NFT 代表的资产。考虑制作宽度在 320 到 1080 像素之间,宽高比在 1.91:1 到 4:5 之间的任何图像"
    }
  },
  
  /** Additional fields for Roles **/
  /** 角色的附加字段 **/

  "roles": [
    {
      "id": {
        "type": "bytes32",
        "description": "Identifies the role"
        // "description": "标识角色"
      },
      "name": {
        "type": "string",
        "description": "Human-readable name of the role"
        // "description": "角色的可读名称"
      },
      "description": {
        "type": "string",
        "description": "Describes the role"
        // "description": "描述角色"
      },
      "inputs": [
        {
          "name": {
            "type": "string",
            "description": "Human-readable name of the argument"
            // "description": "参数的可读名称"
          },
          "type": {
            "type": "string",
            "description": "Solidity type, e.g., uint256 or address"
            // "description": "Solidity 类型,例如,uint256 或 address"
          }
        }
      ]
    }
  ]
  
}

以下 JSON 是 ERC-7432 元数据的示例:

{
  // ... Existing NFT Metadata
  // ... 现有的 NFT 元数据
  
  "roles": [
    {
      // keccak256("PropertyManager()")
      "id": "0x76be0ffb73d8cd9e8fa76c28632ebbc3865a8ec7a0b6acab6ac589a1c88dd301",
      "name": "Property Manager",
      "description": "The manager of the property is responsible for furnishing it and ensuring its good condition.",
      // "description": "房产经理负责提供家具并确保其良好状况。"
      "inputs": []
    },
    {
      // keccak256("PropertyTenant(uint256)")
      "id": "0x17dfc8ea82661b71bd62ce0bd9db3858dd8f3e8ab9799d6ab468ec64f1be21a5",
      "name": "Property Tenant",
      "description": "The tenant of the property is responsible for paying the rent and keeping the property in good condition.",
      // "description": "房产租户负责支付租金并保持房产的良好状况。"
      "inputs": [
        {
          "name": "rent",
          "type": "uint256"
        }
      ]
    }
  ]
  
}

建议使用 roles 数组属性,开发人员应根据需要添加任何其他相关信息(例如,角色的图像)。 同样重要的是要强调 inputs 属性的重要性。 此字段描述了应编码并传递给 grantRole 函数的参数。 建议使用在 Solidity ABI Specification 上定义的属性 typecomponents,其中 type 是参数的规范类型,components 用于复杂的元组类型。

警告

  • 符合规范的合约必须实现 IERC7432 接口。
  • 角色由 bytes32 表示,建议使用角色名称及其输入的 keccak256bytes32 roleId = keccak256("RoleName(input_type)")
  • 如果 expirationDate 是过去的时间,或者 msg.sender 未被批准代表 NFT 所有者授予角色,则 grantRole 函数必须还原。 它可以实现为 publicexternal
  • 除了发出 RoleGranted 事件之外,如果代币被冻结或转移到托管账户,则 grantRole 函数必须发出 TokenLocked 事件。
  • 如果 msg.sender 未被批准代表原始 NFT 所有者或 recipient 撤销角色,则 revokeRole 函数必须还原。 它可以实现为 publicexternal
  • 如果 revocable 为 false,则只有 recipient 可以撤销角色。 如果 revocable 为 true,则 recipient 和原始 NFT 所有者都可以撤销角色。
  • 如果 msg.sender 未被批准,或者至少有一个未过期的不可撤销角色,则 unlockToken 函数必须还原。 它可以实现为 publicexternal
  • setRoleApprovalForAll 函数可以实现为 publicexternal
  • ownerOf 函数可以实现为 pureview,并且必须返回 NFT 原始所有者的地址。
  • recipientOf 函数可以实现为 pureview,并且必须返回接收角色的帐户的地址。
  • roleData 函数可以实现为 pureview,并且必须返回传递给 grantRole 函数的编码数据。
  • roleExpirationDate 函数可以实现为 pureview,并且必须返回给定角色的到期日期。
  • isRoleRevocable 函数可以实现为 pureview,并且必须返回角色是否可撤销。
  • 只有在 _operator 被批准代表原始 NFT 所有者授予和撤销角色时,isRoleApprovedForAll 函数才可以实现为 pureview,并且应仅返回 true
  • 符合规范的合约应实现 ERC-165

理由

ERC-7432 不是 ERC-721 的扩展。 此决定的主要原因是使其能够在外部或与 NFT 相同的合约上实现,从而使 dApp 能够使用不可变的资产来实现角色。 此标准涵盖了许多关键功能,例如自动到期和自定义数据,但也许最重要的是其实现的灵活性。 ERC-7432 可以以多种方式实现,因此,采用了中性术语“锁定”。 此术语可以指 NFT 被冻结(防止转移,直到角色过期)或存入托管合约。 开发人员应根据他们的用例决定使用哪种实现方式。

自动到期

自动到期通过 grantRoleroleExpirationDate 函数实现。 grantRole 负责设置到期日期,而 roleExpirationDate 允许开发人员检查角色是否已过期。 由于大多数编程语言原本不支持 uint256,因此日期在此标准上表示为 uint64uint64 表示的最大 UNIX 时间戳大约是 584,942,417,355 年,这应该足以被认为是“永久的”。 因此,建议使用 type(uint64).max 来支持需要角色永不过期的用例。

可撤销的角色

在某些情况下,NFT 的原始所有者可能需要在到期日期之前撤销角色,而在其他情况下,接收者可能需要确保角色无法撤销。 在 grantRole 函数中引入了 revocable 参数,以指定角色是否可以提前撤销,从而使该标准能够支持这两种用例。

无论 revocable 的值如何,都建议始终允许 recipient 撤销角色,从而消除不需要的分配。

自定义数据

dApp 可以使用 grantRole 函数的 data 参数自定义角色。 data 使用泛型类型 bytes 实现,以使 dApp 能够在授予角色时对任何特定于角色的信息进行编码。 可以使用 roleData 函数检索自定义数据,并使用 RoleGranted 事件发出。 通过这种方法,开发人员可以将此信息集成到他们的应用程序中,无论是在链上还是链下。

角色批准

ERC-721 类似,此标准允许其他帐户代表 NFT 所有者管理角色。 引入此功能是为了允许第三方与 ERC-7432 交互,而无需 NFT 所有权。 符合规范的合约必须实现函数 setRoleApprovalForAllisRoleApprovedForAll 才能实现此功能。

向后兼容性

在所有函数和事件中,该标准都要求同时提供 tokenAddresstokenId。 此要求使 dApp 可以使用独立的 ERC-7432 合约作为不可变 NFT 角色的权威来源。

参考实现

参见 ERC-7432.sol

安全考虑

集成非同质化代币角色接口的开发人员应在其实现中考虑以下事项:

  • 确保已实施适当的访问控制,以防止未经授权的角色分配或撤销。
  • 考虑潜在的攻击媒介,例如重入,并确保已实施适当的保护措施。
  • 批准的帐户应该能够代表另一个用户管理角色。 但是,请确保 NFT 只能转移到托管合约,然后再转移回其原始所有者(而不是转移到批准的帐户)。
  • 在允许用户访问 NFT 的效用之前,请务必检查到期日期。

版权

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

Citation

Please cite this document as:

Ernani São Thiago (@ernanirst), Daniel Lima (@karacurt), "ERC-7432: 非同质化代币角色," Ethereum Improvement Proposals, no. 7432, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7432.