Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-7432: Non-Fungible Token Roles

Role Management for NFTs. Enables accounts to share the utility of NFTs via expirable role assignments.

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

Abstract

This standard introduces role management for NFTs. Each role assignment is associated with a single NFT and expires automatically at a given timestamp. Roles are defined as bytes32 and feature a custom _data field of arbitrary size to allow customization.

Motivation

The NFT Roles interface aims to establish a standard for role management in NFTs. Tracking on-chain roles enables decentralized applications (dApps) to implement access control for privileged actions, e.g., minting tokens with a role (airdrop claim rights).

NFT roles can be deeply integrated with dApps to create a utility-sharing mechanism. A good example is in digital real estate. A user can create a digital property NFT and grant a keccak256("PROPERTY_MANAGER") role to another user, allowing them to delegate specific utility without compromising ownership. The same user could also grant multiple keccak256("PROPERTY_TENANT") roles, allowing the grantees to access and interact with the digital property.

There are also interesting use cases in decentralized finance (DeFi). Insurance policies could be issued as NFTs, and the beneficiaries, insured, and insurer could all be on-chain roles tracked using this standard.

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-7432 Non-Fungible Token Roles
/// @dev See https://eips.ethereum.org/EIPS/eip-7432
/// Note: the ERC-165 identifier for this interface is 0x04984ac8.
interface IERC7432 /* is ERC165 */ {
    struct RoleData {
        uint64 expirationDate;
        bool revocable;
        bytes data;
    }

    struct RoleAssignment {
        bytes32 role;
        address tokenAddress;
        uint256 tokenId;
        address grantor;
        address grantee;
        uint64 expirationDate;
        bytes data;
    }

    /** Events **/

    /// @notice Emitted when a role is granted.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantor The user assigning the role.
    /// @param _grantee 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(
        bytes32 indexed _role,
        address indexed _tokenAddress,
        uint256 indexed _tokenId,
        address _grantor,
        address _grantee,
        uint64 _expirationDate,
        bool _revocable,
        bytes _data
    );

    /// @notice Emitted when a role is revoked.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _revoker The user revoking the role.
    /// @param _grantee The user that receives the role revocation.
    event RoleRevoked(
        bytes32 indexed _role,
        address indexed _tokenAddress,
        uint256 indexed _tokenId,
        address _revoker,
        address _grantee
    );

    /// @notice Emitted when a user is approved to manage any role 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 Grants a role on behalf of a user.
    /// @param _roleAssignment The role assignment data.
    function grantRoleFrom(RoleAssignment calldata _roleAssignment) external;

    /// @notice Grants a role on behalf of a user.
    /// @param _roleAssignment The role assignment data.
    function grantRevocableRoleFrom(RoleAssignment calldata _roleAssignment) external;

    /// @notice Revokes a role on behalf of a user.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _revoker The user revoking the role.
    /// @param _grantee The user that receives the role revocation.
    function revokeRoleFrom(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _revoker,
        address _grantee
    ) external;

    /// @notice Approves operator to grant and revoke any 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 Checks if a user has a role.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantor The user that assigned the role.
    /// @param _grantee The user that received the role.
    function hasNonUniqueRole(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantor,
        address _grantee
    ) external view returns (bool);

    /// @notice Checks if a user has a unique role.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantor The user that assigned the role.
    /// @param _grantee The user that received the role.
    function hasRole(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantor,
        address _grantee
    ) external view returns (bool);

    /// @notice Returns the custom data of a role assignment.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantor The user that assigned the role.
    /// @param _grantee The user that received the role.
    function roleData(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantor,
        address _grantee
    ) external view returns (RoleData memory data_);

    /// @notice Returns the expiration date of a role assignment.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantor The user that assigned the role.
    /// @param _grantee The user that received the role.
    function roleExpirationDate(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantor,
        address _grantee
    ) external view returns (uint64 expirationDate_);

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

    /// @notice Returns the last user to receive a role.
    /// @param _role The role identifier.
    /// @param _tokenAddress The token address.
    /// @param _tokenId The token identifier.
    /// @param _grantor The user that granted the role.
    function lastGrantee(
        bytes32 _role,
        address _tokenAddress,
        uint256 _tokenId,
        address _grantor
    ) external view returns (address);
}

Metadata Extension

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

Updated Metadata Schema:

{
  
  /** Existing NFT Metadata **/

  "title": "Asset Metadata",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Identifies the asset to which this NFT represents"
    },
    "description": {
      "type": "string",
      "description": "Describes the asset to which this NFT represents"
    },
    "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"
    }
  },
  
  /** Additional fields for Roles **/

  "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"
      },
      "isUniqueRole": {
        "type": "boolean",
        "description": "Whether the role supports simultaneous assignments or not"
      },
      "inputs": [
        {
          "name": {
            "type": "string",
            "description": "Human-readable name of the argument"
          },
          "type": {
            "type": "string",
            "description": "Solidity type, e.g., uint256 or address"
          }
        }
      ]
    }
  ]
  
}

The following JSON is an example of ERC-7432 Metadata:

{
  // ... Existing NFT Metadata
  
  "roles": [
    {
      // keccak256("PROPERTY_MANAGER")
      "id": "0x5cefc88e2d50f91b66109b6bb76803f11168ca3d1cee10cbafe864e4749970c7",
      "name": "Property Manager",
      "description": "The manager of the property is responsible for furnishing it and ensuring its good condition.",
      "isUniqueRole": false,
      "inputs": []
    },
    {
      // keccak256("PROPERTY_TENANT")
      "id": "0x06a3b33b0a800805559ee9c64f55afd8a43a05f8472feb6f6b77484ff5ac9c26",
      "name": "Property Tenant",
      "description": "The tenant of the property is responsible for paying the rent and keeping the property in good condition.",
      "isUniqueRole": true,
      "inputs": [
        {
          "name": "rent",
          "type": "uint256"
        }
      ]
    }
  ]
  
}

The roles array properties are SUGGESTED, and developers should add any other relevant information as necessary (e.g., an image for the role). However, it’s highly RECOMMENDED to include the isUniqueRole property, as this field is used to determine if the hasRole or hasNonUniqueRole function should be called (refer to Unique and Non-Unique Roles).

It’s also important to highlight the importance of the inputs property. This field describes the parameters that should be encoded and passed to the grantRole function. It’s RECOMMENDED to use the properties type and components defined on the Solidity ABI Specification, where type is the canonical type of the parameter, and components is used for complex tuple types.

Caveats

  • Compliant contracts MUST implement the IERC7432 interface.
  • A role is represented by a bytes32, and it’s RECOMMENDED to use the keccak256 of the role’s name for this purpose: bytes32 role = keccak256("ROLE_NAME").
  • The grantRoleFrom and grantRevocableRoleFrom functions 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 and revokeRoleFrom functions SHOULD revert if _revocable is false. They MAY be implemented as public or external.
  • The revokeRoleFrom function MUST revert if the msg.sender is not approved to revoke roles on behalf of the _revoker or the _grantee. It MAY be implemented as public or external.
  • The setRoleApprovalForAll function MAY be implemented as public or external.
  • The hasNonUniqueRole function MAY be implemented as pure or view and SHOULD only confirm if the role assignment exists and is not expired.
  • The hasRole function MAY be implemented as pure or view and SHOULD check if the assignment exists, is not expired and is the last one granted (does not support simultaneous role assignments).
  • The roleData function MAY be implemented as pure or view, and SHOULD return an empty struct if the role assignment does not exist.
  • The roleExpirationDate function MAY be implemented as pure or view, and SHOULD return zero if the role assignment does not exist.
  • The isRoleApprovedForAll function MAY be implemented as pure or view and SHOULD only return true if the _operator is approved to grant and revoke roles for all NFTs on behalf of the _grantor.
  • Compliant contracts SHOULD support ERC-165.

Rationale

ERC-7432 IS NOT an extension of ERC-721. The main reason behind this decision is to keep the standard agnostic of any NFT implementation. This approach also enables the standard to be implemented externally or on the same contract as the NFT, and allow dApps to use roles with immutable NFTs.

Automatic Expiration

Automatic expiration is implemented via the grantRole and hasRole functions. grantRole is responsible for setting the expiration date, and hasRole checks if the role is expired by comparing with the current block timestamp (block.timestamp). Since uint256 is not natively supported by most programming languages, dates are represented as uint64 on this standard. The maximum UNIX timestamp represented by a uint64 is about the year 584,942,417,355, which should be enough to be considered “permanent”. For this reason, it’s RECOMMENDED using type(uint64).max when calling the grantRole function to support use cases that require an assignment never to expire.

Revocable Roles

In certain scenarios, the grantor may need to revoke a role before its expiration date, while in others, the grantee requires assurance that the grantor cannot revoke the role. The _revocable parameter was introduced to the grantRole function to support both use cases and specify whether the grantor can revoke assigned roles.

Regardless of the value of _revocable, it’s RECOMMENDED always to enable the grantee to revoke received roles, allowing recipients to eliminate undesirable assignments.

Unique and Non-Unique Roles

The standard supports both unique and non-unique roles. Unique roles can be assigned to only one account at a time, while non-unique roles can be granted to multiple accounts simultaneously. The roles extension metadata can be used to specify if roles are unique or not, and if the contract does not implement it, all roles should be considered unique.

To verify the validity of a unique role, dApps SHOULD use the hasRole function, which also checks if no other assignment was granted afterward. In other words, for unique roles, each new assignment invalidates the previous one, and only the last one can be valid.

Custom Data

DApps can customize roles using the _data parameter of the grantRole function. _data is implemented using the generic type bytes to enable dApps to encode any role-specific information when creating a role assignment. The custom data is retrievable using the roleData function and is emitted with the RoleGranted event. With this approach, developers can integrate this information into their applications, both on-chain and off-chain.

Role Approval

Similar to ERC-721, this standard enables users to approve other accounts to grant and revoke roles on its behalf. This functionality was introduced to allow third-parties to interact with ERC-7432 without requiring NFT ownership. Compliant contracts MUST implement the functions setRoleApprovalForAll and isRoleApprovedForAll to deliver this feature.

Backwards Compatibility

On all functions and events, the standard requires both the tokenAddress and tokenId to be provided. This requirement enables dApps to use a standalone ERC-7432 contract as the authoritative source for the roles of immutable NFTs. It also helps with backward compatibility as NFT-specific functions such as ownerOf are no longer required. Consequently, this design ensures a more straightforward integration with different implementations of NFTs.

Reference Implementation

See ERC-7432.sol.

Security Considerations

Developers integrating the Non-Fungible Token Roles interface should consider the following on their implementations:

  • Ensure proper access controls are in place to prevent unauthorized role assignments or revocations.
  • Take into account potential attack vectors such as reentrancy and ensure appropriate safeguards are in place.
  • Since this standard does not check NFT ownership, it’s the responsibility of the dApp to query for the NFT Owner and pass the correct _grantor to the hasRole function.
  • It’s the responsibility of the dApp to confirm if the role is unique or non-unique. When the role is unique, the dApp SHOULD use the hasRole function to check for the validity of the assignment. hasNonUniqueRole SHOULD be used when the role can be granted to multiple accounts simultaneously (hence, non-unique).

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Ernani São Thiago (@ernanirst), Daniel Lima (@karacurt), "ERC-7432: Non-Fungible Token Roles [DRAFT]," Ethereum Improvement Proposals, no. 7432, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7432.