ERC-7432 - Non-Fungible Token Roles

Created 2023-07-14
Status Final
Category ERC
Type Standards Track
Authors
Requires

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("PropertyManager()") role to another user, allowing them to delegate specific utility without compromising ownership. The same user could also grant a keccak256("PropertyTenant(uint256)") role to other users, allowing the recipient 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 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).
  /// @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.
  /// @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.
  /// @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).
  /// @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.
  /// @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.
  /// @dev Reverts if sender is not approved or the NFT owner.
  /// @param _role The role attributes.
  function grantRole(Role calldata _role) external;

  /// @notice Revokes a role from a user.
  /// @dev Reverts if sender is not approved or the original owner.
  /// @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).
  /// @dev Reverts if sender is not approved or the original owner.
  /// @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.
  /// @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.
  /// @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.
  /// @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.
  /// @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.
  /// @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.
  /// @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.
  /// @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);
}

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. 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"
      },
      "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("PropertyManager()")
      "id": "0x76be0ffb73d8cd9e8fa76c28632ebbc3865a8ec7a0b6acab6ac589a1c88dd301",
      "name": "Property Manager",
      "description": "The manager of the property is responsible for furnishing it and ensuring its good condition.",
      "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.",
      "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). 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

Rationale

ERC-7432 IS NOT an extension of ERC-721. The main reason behind this decision is to enable it to be implemented externally or on the same contract as the NFT, allowing dApps to implement roles with immutable assets. This standard covers many crucial features, such as automatic expiration and custom data, but perhaps the most important one is its flexibility in implementation. ERC-7432 can be implemented in many ways, and for this reason, the neutral term "lock" is employed. This term can refer to an NFT being frozen (preventing transfers until roles expire) or deposited in an escrow contract. Developers should decide which implementation to use based on their use cases.

Automatic Expiration

Automatic expiration is implemented via the grantRole and roleExpirationDate functions. grantRole is responsible for setting the expiration date, and roleExpirationDate allow developers to check whether the role is expired. 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 to support use cases that require a role never to expire.

Revocable Roles

In certain scenarios, the original owner of the NFT may need to revoke a role before its expiration date, while in others, the recipient may require assurance that the role cannot be revoked. The revocable parameter was introduced to the grantRole function to specify whether a role can be revoked prematurely, enabling the standard to support both use cases.

Regardless of the value of revocable, it's recommended always to enable the recipient to revoke roles, allowing them to eliminate undesirable assignments.

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 granting a role. 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 enable other accounts to manage roles on behalf of the NFT owner. 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.

Reference Implementation

See ERC-7432.sol.

Security Considerations

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

Copyright

Copyright and related rights waived via CC0.