EIP-7589: Semi-Fungible Token Roles

, ,

This EIP is the equivalent of ERC-7432 for ERC-1155 type NFTs. you can read more on ERC-7432 at the Ethereum Foundation page.

Abstract

This standard introduces role management for SFTs (Semi-Fungible Tokens). Each role assignment is granted to a single user (grantee) and expires automatically. Roles are defined as bytes32 and feature a custom _data field of arbitrary size to allow customization.

Motivation

ERC-1155 has significantly contributed to the tokenization capabilities of Ethereum by enabling developers to create fungible and non-fungible tokens with a single contract. While ERC-1155 excels at tracking ownership, it focuses solely on token balances, overlooking the nuanced aspects of how these tokens can be utilized.

An essential aspect of token utility is access control, which determines who has permission to spend or use these tokens. In some cases, the owner has complete control over its balance. Nevertheless, in many others, the utility can be delegated (or granted) to other users, allowing for more complex use cases to be implemented.

One example is in gaming, in-game assets can be issued with a single ERC-1155 contract and rented out via a secure role management interface.

Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC-2119 and RFC-8174.

Compliant contracts MUST implement the following interface:

/// @title ERC-7589 Semi-Fungible Token Roles
/// @dev See https://eips.ethereum.org/EIPS/eip-7589
/// Note: the ERC-165 identifier for this interface is 0xc4c8a71d.
interface IERC7589 /* is ERC165 */ {

    /** Events **/

    /// @notice Emitted when tokens are committed (deposited or frozen).
    /// @param _grantor The owner of the SFTs.
    /// @param _commitmentId The identifier of the commitment created.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _tokenAmount The token amount.
    event TokensCommitted(
        address indexed _grantor,
        uint256 indexed _commitmentId,
        address indexed _tokenAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    );

    /// @notice Emitted when a role is granted.
    /// @param _commitmentId The commitment identifier.
    /// @param _role The role identifier.
    /// @param _grantee The recipient 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(
        uint256 indexed _commitmentId,
        bytes32 indexed _role,
        address indexed _grantee,
        uint64 _expirationDate,
        bool _revocable,
        bytes _data
    );

    /// @notice Emitted when a role is revoked.
    /// @param _commitmentId The commitment identifier.
    /// @param _role The role identifier.
    /// @param _grantee The recipient of the role revocation.
    event RoleRevoked(uint256 indexed _commitmentId, bytes32 indexed _role, address indexed _grantee);

    /// @notice Emitted when a user releases tokens from a commitment.
    /// @param _commitmentId The commitment identifier.
    event TokensReleased(uint256 indexed _commitmentId);

    /// @notice Emitted when a user is approved to manage roles on behalf of another user.
    /// @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 _isApproved);

    /** External Functions **/

    /// @notice Commits tokens (deposits on a contract or freezes balance).
    /// @param _grantor The owner of the SFTs.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _tokenAmount The token amount.
    /// @return commitmentId_ The unique identifier of the commitment created.
    function commitTokens(
        address _grantor,
        address _tokenAddress,
        uint256 _tokenId,
        uint256 _tokenAmount
    ) external returns (uint256 commitmentId_);

    /// @notice Grants a role to `_grantee`.
    /// @param _commitmentId The identifier of the commitment.
    /// @param _role The role identifier.
    /// @param _grantee The recipient 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.
    function grantRole(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee,
        uint64 _expirationDate,
        bool _revocable,
        bytes calldata _data
    ) external;

    /// @notice Revokes a role.
    /// @param _commitmentId The commitment identifier.
    /// @param _role The role identifier.
    /// @param _grantee The recipient of the role revocation.
    function revokeRole(uint256 _commitmentId, bytes32 _role, address _grantee) external;

    /// @notice Releases tokens back to grantor.
    /// @param _commitmentId The commitment identifier.
    function releaseTokens(uint256 _commitmentId) external;

    /// @notice Approves operator to grant and revoke roles on behalf of another user.
    /// @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 Returns the owner of the commitment (grantor).
    /// @param _commitmentId The commitment identifier.
    /// @return grantor_ The commitment owner.
    function grantorOf(uint256 _commitmentId) external view returns (address grantor_);

    /// @notice Returns the address of the token committed.
    /// @param _commitmentId The commitment identifier.
    /// @return tokenAddress_ The token address.
    function tokenAddressOf(uint256 _commitmentId) external view returns (address tokenAddress_);

    /// @notice Returns the identifier of the token committed.
    /// @param _commitmentId The commitment identifier.
    /// @return tokenId_ The token identifier.
    function tokenIdOf(uint256 _commitmentId) external view returns (uint256 tokenId_);

    /// @notice Returns the amount of tokens committed.
    /// @param _commitmentId The commitment identifier.
    /// @return tokenAmount_ The token amount.
    function tokenAmountOf(uint256 _commitmentId) external view returns (uint256 tokenAmount_);

    /// @notice Returns the custom data of a role assignment.
    /// @param _commitmentId The commitment identifier.
    /// @param _role The role identifier.
    /// @param _grantee The recipient the role.
    /// @return data_ The custom data.
    function roleData(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee
    ) external view returns (bytes memory data_);

    /// @notice Returns the expiration date of a role assignment.
    /// @param _commitmentId The commitment identifier.
    /// @param _role The role identifier.
    /// @param _grantee The recipient the role.
    /// @return expirationDate_ The expiration date.
    function roleExpirationDate(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee
    ) external view returns (uint64 expirationDate_);

    /// @notice Returns the expiration date of a role assignment.
    /// @param _commitmentId The commitment identifier.
    /// @param _role The role identifier.
    /// @param _grantee The recipient the role.
    /// @return revocable_ Whether the role is revocable or not.
    function isRoleRevocable(
        uint256 _commitmentId,
        bytes32 _role,
        address _grantee
    ) external view returns (bool revocable_);

    /// @notice Checks if the grantor approved the operator for all SFTs.
    /// @param _tokenAddress The token address.
    /// @param _grantor The user that approved the operator.
    /// @param _operator The user that can grant and revoke roles.
    /// @return isApproved_ Whether the operator is approved or not.
    function isRoleApprovedForAll(
        address _tokenAddress,
        address _grantor,
        address _operator
    ) external view returns (bool isApproved_);
}

Single Transaction Extension

Granting roles is a two-step process that requires two transactions. The first is to commit tokens, and the second is to grant the role. This extension allows users to commit tokens and grant a role in one transaction, which is desirable for some use cases.

/// @title ERC-7589 Semi-Fungible Token Roles, optional single transaction extension
/// @dev See https://eips.ethereum.org/EIPS/eip-7589
/// Note: the ERC-165 identifier for this interface is 0x5c3d7d74.
interface ICommitTokensAndGrantRoleExtension /* is ERC7589 */ {
    /// @notice Commits tokens and grant role in a single transaction.
    /// @param _grantor The owner of the SFTs.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _tokenAmount The token amount.
    /// @param _role The role identifier.
    /// @param _grantee The recipient 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.
    /// @return commitmentId_ The identifier of the commitment created.
    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_);
}

Role Balance Extension

The core interface allows for querying a token commitment’s balance but not for a specific user’s balance. To determine the total amount of tokens granted to a user, the implementation needs to sum up all the roles granted to that user while filtering out any expired roles.

This function was included in an optional extension because it’s not always necessary and will likely make the implementation much more complex (increasing smart contract risk).

/// @title ERC-7589 Semi-Fungible Token Roles, optional role balance extension
/// @dev See https://eips.ethereum.org/EIPS/eip-7589
/// Note: the ERC-165 identifier for this interface is 0x2f35b73f.
interface IRoleBalanceOfExtension /* is ERC7589 */ {
    /// @notice Returns the sum of all tokenAmounts granted to the grantee for the given role.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantee The user for which the balance is returned.
    /// @return balance_ The balance of the grantee for the given role.
    function roleBalanceOf(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantee
    ) external returns (uint256 balance_);
}

Metadata Extension

The Roles Metadata extension extends the traditional JSON-based metadata schema of SFTs. Therefore, DApps supporting this feature MUST also implement the metadata extension of ERC-1155. This extension is optional and allows developers to provide additional information on roles.

Updated Metadata Schema:

{

    /** Existing ERC-1155 Metadata **/

    "title": "Token Metadata",
    "type": "object",
    "properties": {
        "name": {
          "type": "string",
          "description": "Identifies the asset to which this token represents"
        },
        "decimals": {
          "type": "integer",
          "description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
        },
        "description": {
          "type": "string",
          "description": "Describes the asset to which this token represents"
        },
        "image": {
          "type": "string",
          "description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        },
        "properties": {
          "type": "object",
          "description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
        }
    },

    /** Additional fields for ERC-7589 **/

    "roles": [{
        "id": {
            "type": "bytes32",
            "description": "Identifies the role"
        },
        "name": {
            "type": "string",
            "description": "Human-readable name of the role"
        },
        "description": {
            "type": "string",
            "description": "Describes the role"
        },
        "inputs": [{
          "name": {
            "type": "string",
            "description": "Human-readable name of the argument"
          },
          "type": {
            "type": "string",
            "description": "Solidity type, e.g., uint256 or address"
          }
        }]
    }]
}

The following code snipped is an example of the additional fields described above:

{
  // ... Existing SFT Metadata

  "roles": [
    {
      // keccak256("Player(uint256)")
      "id": "0x70d2dab8c6ff873dc0b941220825d9271fdad6fdb936f6567ffde77d05491cef",
      "name": "Player",
      "description": "The user allowed to use this item in-game.",
      "inputs": [
        {
          "name": "ProfitShare",
          "type": "uint256"
        }
      ]
    }
  ]
}

The properties of the roles array are SUGGESTED, and developers should add any other relevant information for their use case (e.g., an image representing the role).

It’s also important to highlight the significance of the inputs property. This field describes the parameters that should be encoded and passed to the grantRole function, and can include the properties type and components to represent the format of the data. It’s RECOMMENDED to use the properties type and components as defined on the Solidity ABI Specification.

Caveats

  • Compliant contracts MUST implement the IERC7589 interface.
  • Every role is represented by a bytes32 identifier. It’s RECOMMENDED to use the keccak256 hash of the role name and its arguments (if any) as the identifier. E.g., keccak256("Player(uint256)").
  • The commitTokens function MUST revert if the _tokenAmount is zero or the msg.sender was not approved by the _grantor. It MAY be implemented as public or external.
  • The grantRole function MUST revert if the _expirationDate is in the past or if the msg.sender is not approved to
    grant roles on behalf of the grantor. It MAY be implemented as public or external.
  • The revokeRole function MAY be implemented as public or external and MUST revert if:
    • The role assignment is not found (no role was granted).
    • The msg.sender was not approved by the grantor or the grantee.
    • The msg.sender is the grantor or was approved by the grantor, but the role is not revocable or expired.
  • The releaseTokens function MAY be implemented as public or external and MUST revert if:
    • The commitment is not found (no tokens were committed).
    • The msg.sender is not and was not approved by the grantor.
    • The commitment has at least one non-revocable role that didn’t expire.
  • The setRoleApprovalForAll function MAY be implemented as public or external.
  • The grantorOf function MAY be implemented as pure or view and MUST return the owner of the committed tokens.
  • The tokenAddressOf function MAY be implemented as pure or view and MUST return the address of the committed tokens.
  • The tokenIdOf function MAY be implemented as pure or view and MUST return the identifier of the committed tokens.
  • The tokenAmountOf function MAY be implemented as pure or view and MUST return the token amount committed.
  • The roleData function MAY be implemented as pure or view and MUST return the custom data of the role assignment.
  • The roleExpirationDate function MAY be implemented as pure or view and MUST return the expiration date of the role assignment.
  • The isRoleRevocable function MAY be implemented as pure or view and MUST return whether the grantor can end the role assignment before its expiration date.
  • The isRoleApprovedForAll function MAY be implemented as pure or view and MUST return whether the _operator is allowed to grant and revoke roles on behalf of the _grantor.

Please note that “approval” refers to allowing users to commit tokens and grant/revoke roles on one’s behalf. An approved user either received the role approval or is the target user. Role approvals are not to be confused with ERC-1155 approvals. More information can be found in the Role Approvals section.

Rationale

The conventional methods for utilizing SFTs often involve transferring tokens to another address or freezing balances to restrict transfers. However, the question arises: How can SFT owners securely delegate the utility of their assets without compromising custody?

ERC-7589 steps in as a solution by introducing the concept of “token commitments”. This abstraction serves as a powerful tool for users looking to delegate the control of their SFTs. A token commitment can represent either a frozen balance or tokens deposited into a contract, offering a standardized and secure way for SFT owners to delegate the use of their assets. Through ERC-7589, users gain a versatile mechanism to abstract the complexities of secure delegation, enhancing the utility and interoperability of semi-fungible tokens.

ERC-7589 IS NOT an extension of ERC-1155. The main reason behind this decision is to keep the standard agnostic of any implementation. This approach enables the standard to be implemented externally or on the same contract as the SFT and allows dApps to use roles with immutable SFTs.

Role Approvals

Like ERC-1155, ERC-7589 allows users to approve operators to grant and revoke roles on their behalf. This feature is crucial for interoperability, as it enables third-party applications to manage user roles without custody-level approvals. Role approvals are part of the core interface, and compliant contracts MUST implement the setRoleApprovalForAll and isRoleApprovedForAll functions.

Automatic Expiration

Automatic expiration is implemented to save users gas. To end a role assignment, instead of requiring users always to call revokeRole, applications SHOULD call the roleExpirationDate and compare it to the current timestamp to check if the role is still valid.

In the context of ERC-7589, dates are represented as uint64. The maximum UNIX timestamp represented by a uint64 is about the year 584 billion, which should be enough to be considered “permanent”. For this reason, it’s RECOMMENDED using type(uint64).max in use cases where the assignment is required never to expire.

Revocable Roles

In certain scenarios, the grantor might need to revoke a role before its expiration. While in others, the grantee requires assurance that the role can’t be prematurely revoked (e.g. when the grantee pays tokens to utilize them). The _revocable parameter was included in the grantRole function for this exact reason, and it specifies whether the grantor can revoke the role prior to the expiration date. Regardless of the _revocable value, the grantee SHOULD always be able to revoke roles, allowing recipients to eliminate undesirable assignments.

Custom Data

The grantRole function’s _data parameter is critical for the standardization of this EIP. SFTs have different use cases, and it’s impractical to attempt to cover all of them on a solidity-level interface. Therefore, a generic parameter of type bytes was incorporated, allowing users to pass any custom information when granting a role.

For example, it’s common for web3 games to introduce a profit-share when delegating NFTs to players, which is represented by a uint256. Using ERC-7589, one could simply encode the uint256 as bytes and pass it to thegrantRole function. Data validation can happen on-chain or off-chain, and other contracts can query this information using the roleData function.

Backwards Compatibility

Many SFTs are deployed as immutable contracts, which imposes the following challenge: How can one enable role management for SFTs that can’t be modified? ERC-7589 solves this problem by requiring the tokenAddress parameter when committing tokens. This requirement ensures that dApps can either implement ERC-7589 inside the SFT contract or use a standalone external contract as the authoritative source for the roles of immutable SFTs.

Security Considerations

Developers integrating with Semi-Fungible Token Roles should consider the points below on their implementations:

  • Ensure proper access control is in place to prevent unauthorized role assignments or revocations. This is especially
    important in commitTokens and releaseTokens, as they might freeze or transfer balances.
  • Consider potential attack vectors such as reentrancy and ensure appropriate safeguards are in place.
  • Always check the expiration date before allowing users to utilize a role assignment.
2 Likes

ERC-7589 reference implementation is available here!

2 Likes

Thanks ernani for this ERC draft!

I wonder if you have searched prior related ERCs that maybe relevant to consider depend upon?

Hi @xinbenlv

Yes, we have searched previous ERCs. The closest one we found was ERC-5982, which also allows users to grant and revoke roles. However, in this EIP, roles are associated with a TokenCommitment, a new concept we introduced to represent either a frozen token balance or tokens deposited on a contract. Since every method requires the commitmentId to be passed, it was difficult to extend ERC-5982.

We also explored making ERC-1155 a requirement. The challenge is that it’s crucial to allow existing immutable implementations of ERC-1155 to support role management for NFTs. The solution was not to extend ERC-1155 and enable developers to create standalone implementations of this EIP (outside the ERC-1155 contract).