Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7589: 半同质化代币角色

半同质化代币(SFT)的角色管理。允许帐户通过过期的角色分配来共享 SFT 的效用。

Authors Ernani São Thiago (@ernanirst), Daniel Lima (@karacurt)
Created 2023-12-28
Discussion Link https://ethereum-magicians.org/t/eip-7589-semi-fungible-token-roles/17967
Requires EIP-165, EIP-1155

摘要

本标准引入了 SFT(半同质化代币)的角色管理。每个角色分配都授予给单个用户(被授予者)并自动过期。角色被定义为 bytes32,并具有任意大小的自定义 _data 字段以允许自定义。

动机

ERC-1155 通过使开发者能够使用单个合约创建同质化和非同质化代币,从而极大地促进了以太坊的代币化能力。ERC-1155 在跟踪所有权方面表现出色,但它仅关注代币余额,而忽略了这些代币如何使用的细微之处。

代币效用的一个重要方面是访问控制,它决定了谁有权花费或使用这些代币。在某些情况下,所有者对其余额拥有完全控制权。然而,在许多其他情况下,效用可以委派(或授予)给其他用户,从而可以实现更复杂的用例。

一个例子是在游戏中,游戏内资产可以通过单个 ERC-1155 合约发行,并通过安全的角色管理界面出租。

规范

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

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

/// @title ERC-7589 半同质化代币角色
/// @dev 见 https://eips.ethereum.org/EIPS/eip-7589
/// 注意:此接口的 ERC-165 标识符是 0xc4c8a71d。
interface IERC7589 /* is IERC165 */ {

    /** 事件 **/

    /// @notice 当代币被提交(存入或冻结)时发出。
    /// @param _grantor SFT 的所有者。
    /// @param _commitmentId 创建的承诺的标识符。
    /// @param _tokenAddress 代币地址。
    /// @param _tokenId 代币标识符。
    /// @param _tokenAmount 代币数量。
    event TokensCommitted(
        address indexed _grantor,
        uint256 indexed _commitmentId,
        address indexed _tokenAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    );

    /// @notice 当角色被授予时发出。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色的接收者。
    /// @param _expirationDate 角色的到期日期。
    /// @param _revocable 该角色是否可撤销。
    /// @param _data 关于角色的任何其他数据。
    event RoleGranted(
        uint256 indexed _commitmentId,
        bytes32 indexed _role,
        address indexed _grantee,
        uint64 _expirationDate,
        bool _revocable,
        bytes _data
    );

    /// @notice 当角色被撤销时发出。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色撤销的接收者。
    event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee);

    /// @notice 当用户从承诺中释放代币时发出。
    /// @param _commitmentId 承诺标识符。
    event TokensReleased(uint256 indexed _commitmentId);

    /// @notice 当用户被批准代表另一个用户管理角色时发出。
    /// @param _tokenAddress 代币地址。
    /// @param _operator 被批准授予和撤销角色的用户。
    /// @param _isApproved 批准状态。
    event RoleApprovalForAll(address indexed _tokenAddress, address indexed _operator, bool _isApproved);

    /** 外部函数 **/

    /// @notice 提交代币(在合约上存入或冻结余额)。
    /// @param _grantor SFT 的所有者。
    /// @param _tokenAddress 代币地址。
    /// @param _tokenId 代币标识符。
    /// @param _tokenAmount 代币数量。
    /// @return commitmentId_ 创建的承诺的唯一标识符。
    function commitTokens(
        address _grantor,
        address _tokenAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    ) external returns (uint256 commitmentId_);

    /// @notice 将角色授予 `_grantee`。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色的接收者。
    /// @param _expirationDate 角色的到期日期。
    /// @param _revocable 该角色是否可撤销。
    /// @param _data 关于角色的任何其他数据。
    function grantRole(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee,
        uint64 _expirationDate,
        bool _revocable,
        bytes calldata _data
    ) external;

    /// @notice 撤销角色。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色撤销的接收者。
    function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external;

    /// @notice 将代币释放回授予者。
    /// @param _commitmentId 承诺标识符。
    function releaseTokens(uint256 _commitmentId) external;

    /// @notice 批准操作员代表另一个用户授予和撤销角色。
    /// @param _tokenAddress 代币地址。
    /// @param _operator 被批准授予和撤销角色的用户。
    /// @param _approved 批准状态。
    function setRoleApprovalForAll(address _tokenAddress, address _operator, bool _approved) external;

    /** 视图函数 **/

    /// @notice 返回 承诺的所有者(授予者)。
    /// @param _commitmentId 承诺标识符。
    /// @return grantor_ 承诺所有者。
    function grantorOf(uint256 _commitmentId) external view returns (address grantor_);

    /// @notice 返回已提交的代币的地址。
    /// @param _commitmentId 承诺标识符。
    /// @return tokenAddress_ 代币地址。
    function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_);

    /// @notice 返回已提交的代币的标识符。
    /// @param _commitmentId 承诺标识符。
    /// @return tokenId_ 代币标识符。
    function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_);

    /// @notice 返回已提交的代币数量。
    /// @param _commitmentId 承诺标识符。
    /// @return tokenAmount_ 代币数量。
    function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_);

    /// @notice 返回角色分配的自定义数据。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色的接收者。
    /// @return data_ 自定义数据。
    function roleData(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee
    ) external view returns (bytes memory data_);

    /// @notice 返回角色分配的到期日期。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色的接收者。
    /// @return expirationDate_ 到期日期。
    function roleExpirationDate(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee
    ) external view returns (uint64 expirationDate_);

    /// @notice 返回角色分配的到期日期。
    /// @param _commitmentId 承诺标识符。
    /// @param _role 角色标识符。
    /// @param _grantee 角色的接收者。
    /// @return revocable_ 该角色是否可撤销。
    function isRoleRevocable(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee
    ) external view returns (bool revocable_);

    /// @notice 检查授予者是否批准了所有 SFT 的操作员。
    /// @param _tokenAddress 代币地址。
    /// @param _grantor 批准操作员的用户。
    /// @param _operator 可以授予和撤销角色的用户。
    /// @return isApproved_ 操作员是否被批准。
    function isRoleApprovedForAll(
        address _tokenAddress,
        address _grantor,
        address _operator
    ) external view returns (bool isApproved_);
}

单笔交易扩展

授予角色是一个两步过程,需要两笔交易。首先是提交代币,第二步是 授予角色。此扩展允许用户在一次交易中提交代币并授予角色,这对于某些用例来说是可取的。

/// @title ERC-7589 半同质化代币角色,可选的单笔交易扩展
/// @dev 见 https://eips.ethereum.org/EIPS/eip-7589
/// 注意:此接口的 ERC-165 标识符是 0x5c3d7d74。
interface ICommitTokensAndGrantRoleExtension /* is IERC7589 */ {
    /// @notice 在单笔交易中提交代币并授予角色。
    /// @param _grantor SFT 的所有者。
    /// @param _tokenAddress 代币地址。
    /// @param _tokenId 代币标识符。
    /// @param _tokenAmount 代币数量。
    /// @param _role 角色标识符。
    /// @param _grantee 角色的接收者。
    /// @param _expirationDate 角色的到期日期。
    /// @param _revocable 该角色是否可撤销。
    /// @param _data 关于角色的任何其他数据。
    /// @return commitmentId_ 创建的承诺的标识符。
    function commitTokensAndGrantRole(
        address _grantor,
        address _tokenAddress,
        uint256 _tokenId,
        uint256 _tokenAmount,
        bytes32 _role,
        address _grantee,
        uint64 _expirationDate,
        bool _revocable,
        bytes calldata _data
    ) external returns (uint256 commitmentId_);
}

角色余额扩展

核心接口允许查询代币承诺的余额,但不允许查询特定用户的余额。要确定授予用户的代币总额,实现需要将授予该用户的 所有角色加起来,同时过滤掉任何过期的角色。

此函数包含在可选扩展中,因为它并非总是必需的,并且可能会使实现更加复杂(增加智能合约风险)。

/// @title ERC-7589 半同质化代币角色,可选的角色余额扩展
/// @dev 见 https://eips.ethereum.org/EIPS/eip-7589
/// 注意:此接口的 ERC-165 标识符是 0x2f35b73f。
interface IRoleBalanceOfExtension /* is IERC7589 */ {
    /// @notice 返回授予被授予者给定角色的所有 tokenAmounts 的总和。
    /// @param _role 角色标识符。
    /// @param _tokenAddress 代币地址。
    /// @param _tokenId 代币标识符。
    /// @param _grantee 返回余额的用户。
    /// @return balance_ 被授予者给定角色的余额。
    function roleBalanceOf(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantee
    ) external returns (uint256 balance_);
}

元数据扩展

角色元数据扩展扩展了 SFT 的传统 JSON 格式的元数据模式。因此,支持 此功能的 DApp 还必须实现 ERC-1155 的元数据扩展。此 JSON 扩展是 可选的,允许开发者提供有关角色的其他信息。

更新的 JSON 模式:

{

  /** 现有的 ERC-1155 元数据 **/

  "title": "代币元数据",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "标识此代币代表的资产"
    },
    "decimals": {
      "type": "integer",
      "description": "代币数量应显示的十进制位数 - 例如,18 表示将代币数量除以 1000000000000000000 以获得其用户表示。"
    },
    "description": {
      "type": "string",
      "description": "描述此代币代表的资产"
    },
    "image": {
      "type": "string",
      "description": "指向资源且 mime 类型为 image/* 的 URI,表示此代币代表的资产。考虑将任何图像的宽度设置为 320 到 1080 像素之间,宽高比设置为 1.91:1 到 4:5(含)。"
    },
    "properties": {
      "type": "object",
      "description": "任意属性。值可以是字符串、数字、对象或数组。"
    }
  },

  /** ERC-7589 的附加字段 **/

  "roles": [{
    "id": {
      "type": "bytes32",
      "description": "标识角色"
    },
    "name": {
      "type": "string",
      "description": "角色的人类可读名称"
    },
    "description": {
      "type": "string",
      "description": "描述角色"
    },
    "inputs": [{
      "name": {
        "type": "string",
        "description": "参数的人类可读名称"
      },
      "type": {
        "type": "string",
        "description": "Solidity 类型,例如 uint256 或 address"
      }
    }]
  }]
}

以下代码片段是上述附加字段的示例:

{

  /** 现有的 ERC-1155 元数据 **/

  "name": "资产名称",
  "description": "Lorem ipsum...",
  "image": "https:\/\/s3.amazonaws.com\/your-bucket\/images\/{id}.png",
  "properties": {
    "simple_property": "示例值",
    "rich_property": {
      "name": "名称",
      "value": "123",
      "display_value": "123 示例值",
      "class": "emphasis",
      "css": {
        "color": "#ffffff",
        "font-weight": "bold",
        "text-decoration": "underline"
      }
    },
    "array_property": {
      "name": "名称", 
      "value": [1,2,3,4],
      "class": "emphasis"
    }
  },

  /** ERC-7589 的附加字段 **/

  "roles": [
    {
      // keccak256("Player(uint256)")
      "id": "0x70d2dab8c6ff873dc0b941220825d9271fdad6fdb936f6567ffde77d05491cef",
      "name": "玩家",
      "description": "允许用户在游戏中使用此物品。",
      "inputs": [
        {
          "name": "利润分成",
          "type": "uint256"
        }
      ]
    }
  ]
}

roles 数组的属性是建议的,开发者应为其用例添加任何其他相关信息(例如,表示角色的图像)。

同样重要的是要强调 inputs 属性的重要性。此字段描述了应编码并传递给 grantRole 函数的参数,并且可以包括属性 typecomponents 以表示数据的格式。建议使用 Solidity ABI 规范中定义的属性 typecomponents

注意事项

  • 符合标准的合约必须实现 IERC7589 接口。
  • 每个角色都由 bytes32 标识符表示。建议使用角色名称及其参数(如果有)的 keccak256 哈希作为标识符。例如,keccak256("Player(uint256)")
  • 如果 _tokenAmount 为零或 msg.sender 未经 _grantor 批准,则 commitTokens 函数必须回退。它可以实现为 public 或 external。
  • 如果 _expirationDate 在过去,或者 msg.sender 未被批准代表授予者授予角色,则 grantRole 函数必须回退。它可以实现为 public 或 external,并且建议对永久角色使用 type(uint64).max
  • revokeRole 函数应始终允许被授予者撤销角色,并且可以实现为 public 或 external,如果发生以下情况,则必须回退:
    • 未找到角色分配(未授予任何角色)。
    • msg.sender 未经授予者或被授予者批准。
    • msg.sender 是授予者或经授予者批准,但角色不可撤销或已过期。
  • releaseTokens 函数可以实现为 public 或 external,如果发生以下情况,则必须回退:
    • 未找到承诺(未提交任何代币)。
    • msg.sender 不是且未经授予者批准。
    • 承诺至少有一个未过期且不可撤销的角色。
  • setRoleApprovalForAll 函数可以实现为 public 或 external。
  • grantorOf 函数可以实现为 pure 或 view,并且必须返回已提交代币的所有者。
  • tokenAddressOf 函数可以实现为 pure 或 view,并且必须返回已提交代币的地址。
  • tokenIdOf 函数可以实现为 pure 或 view,并且必须返回已提交代币的标识符。
  • tokenAmountOf 函数可以实现为 pure 或 view,并且必须返回已提交的代币数量。
  • roleData 函数可以实现为 pure 或 view,并且必须返回角色分配的自定义数据。
  • roleExpirationDate 函数可以实现为 pure 或 view,并且必须返回角色分配的到期日期。
  • isRoleRevocable 函数可以实现为 pure 或 view,并且必须返回授予者是否可以在到期日期之前结束角色分配。
  • isRoleApprovedForAll 函数可以实现为 pure 或 view,并且必须返回 _operator 是否被允许代表 _grantor 授予和撤销角色。

请注意,“批准”是指允许用户代表自己提交代币和授予/撤销角色。经过批准的用户要么收到了角色批准,要么是目标用户。角色批准不应与 ERC-1155 批准混淆。更多信息可以在 角色批准 部分找到。

原理

“代币承诺”的概念作为一种抽象,是用户寻求委派其 SFT 控制权的强大工具。代币承诺表示冻结的余额或存入合约的代币,从而为 SFT 所有者提供了一种标准化且安全的方式来委派其资产的使用。通过 ERC-7589,用户获得了一种通用的机制来抽象安全委派的复杂性,从而增强了半同质化代币的效用和互操作性。

ERC-7589 不是 ERC-1155 的扩展。此决定背后的主要原因是保持标准与任何实现无关。这种方法使标准可以在外部或与 SFT 相同的合约上实现,并允许 dApp 将角色与不可变的 SFT 一起使用。

角色批准

ERC-1155 类似,ERC-7589 允许用户批准操作员代表自己授予和撤销角色。此功能对于互操作性至关重要,因为它使第三方应用程序能够在没有托管级别批准的情况下管理用户角色。角色批准是核心接口的一部分,符合标准的合约必须实现 setRoleApprovalForAllisRoleApprovedForAll 函数。

自动过期

实施自动过期的目的是为了节省用户的 gas 费。为了结束角色分配,应用程序不应要求用户始终 调用 revokeRole,而应调用 roleExpirationDate 并将其与当前时间戳进行比较,以检查角色是否仍然有效。

ERC-7589 的上下文中,日期表示为 uint64uint64 表示的最大 UNIX 时间戳约为 5840 亿年,这应该足以被认为是“永久的”。因此,在分配中使用 type(uint64).max 表示它永不过期。

可撤销角色

在某些情况下,授予者可能需要在角色到期之前撤销角色。而在其他情况下,被授予者 需要保证角色不会被过早撤销(例如,当被授予者支付代币以使用它们时)。_revocable 参数包含在 grantRole 函数中正是出于这个原因,它指定了授予者是否可以在到期日期之前撤销角色。无论 _revocable 值如何,被授予者始终 能够撤销角色,从而允许接收者取消不需要的分配。

自定义数据

grantRole 函数的 _data 参数对于此 EIP 的标准化至关重要。SFT 有不同的用例,并且尝试在 solidity 级别的接口上涵盖所有用例是不切实际的。因此,合并了一个 bytes 类型的通用 参数,允许用户在授予角色时传递任何自定义信息。

例如,web3 游戏通常在将 NFT 委托给玩家时引入利润分成,这由 uint256 表示。使用 ERC-7589,可以简单地将 uint256 编码为字节并将其传递给 grantRole 函数。数据验证可以在链上或链下进行,其他合约可以使用 roleData 函数查询此信息。

向后兼容性

许多 SFT 都部署为不可变的合约,这带来了以下挑战:如何为无法修改的 SFT 启用角色管理?本提案通过在提交代币时需要 tokenAddress 参数来解决此问题。此要求确保 dApp 可以在 SFT 合约内部实现 ERC-7589,或者使用独立的外部合约作为不可变 SFT 角色的权威来源。

参考实现

参见 ERC7589.sol

安全考虑

与半同质化代币角色集成的开发者应在其实现中考虑以下几点:

  • 确保实施适当的访问控制,以防止未经授权的角色分配或撤销。这在 commitTokensreleaseTokens 中尤其重要,因为它们可能会冻结或转移余额。
  • 考虑潜在的攻击媒介,例如重入,并确保采取适当的保护措施。
  • 始终在允许用户使用角色分配之前检查到期日期。

版权

CC0 下放弃版权及相关权利。

Citation

Please cite this document as:

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