Abstract
This standard introduces role management for NFTs. Each role assignment is associated with a single NFT and expires automatically at a given timestamp. Inspired by ERC-5982, roles are defined as bytes32 and feature a custom _data field of arbitrary size to allow customization.
https://github.com/ethereum/EIPs/pull/7432
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.
ERC-7432 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 0x851f3b3f.
interface IERC7432 /* is ERC165 */ {
/// @notice Emitted when a role is granted.
/// @param _role The role identifier.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _grantee The user that receives the role assignment.
/// @param _expirationDate The expiration date of the role assignment.
/// @param _data Any additional data about the role assignment.
event RoleGranted(
bytes32 indexed _role,
address indexed _tokenAddress,
uint256 indexed _tokenId,
address _grantee,
uint64 _expirationDate,
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 _grantee The user that receives the role revocation.
event RoleRevoked(
bytes32 indexed _role,
address indexed _tokenAddress,
uint256 indexed _tokenId,
address _grantee
);
/// @notice Grants a role to a user.
/// @param _role The role identifier.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _grantee The user that receives the role assignment.
/// @param _expirationDate The expiration date of the role assignment.
/// @param _data Any additional data about the role assignment.
function grantRole(
bytes32 _role,
address _tokenAddress,
uint256 _tokenId,
address _grantee,
uint64 _expirationDate,
bytes calldata _data
) external;
/// @notice Revokes a role from a user.
/// @param _role The role identifier.
/// @param _tokenAddress The token address.
/// @param _tokenId The token identifier.
/// @param _grantee The user that receives the role revocation.
function revokeRole(
bytes32 _role,
address _tokenAddress,
uint256 _tokenId,
address _grantee
) external;
/// @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 role creator.
/// @param _grantee The user that receives the role.
/// @param _supportsMultipleAssignments if false, will return true only if account is the last role grantee.
function hasRole(
bytes32 _role,
address _tokenAddress,
uint256 _tokenId,
address _grantor,
address _grantee,
bool _supportsMultipleAssignments
) 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 role creator.
/// @param _grantee The user that receives the role.
function roleData(
bytes32 _role,
address _tokenAddress,
uint256 _tokenId,
address _grantor,
address _grantee
) external view returns (bytes 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 role creator.
/// @param _grantee The user that receives the role.
function roleExpirationDate(
bytes32 _role,
address _tokenAddress,
uint256 _tokenId,
address _grantor,
address _grantee
) external view returns (uint64 expirationDate_);
}
Caveats
- Compliant contracts MUST implement the
IERC7432interface. - A role is represented by a
bytes32, and it’s RECOMMENDED to use thekeccak256of the role’s name for this purpose:bytes32 role = keccak256("ROLE_NAME"). grantRolefunction MUST revert if the_expirationDateis in the past, and MAY be implemented aspublicorexternal.revokeRolefunction MAY be implemented aspublicorexternal.- The
hasRolefunction MAY be implemented aspureorview. - The
hasRolefunction SHOULD returnfalseif_supportsMultipleAssignmentsisfalseand last role assignment is not to_grantee(see Unique and Non-Unique Roles for more). - The
roleDatafunction MAY be implemented aspureorview. - Compliant contracts SHOULD support ERC-165.
Rationale
ERC-7432 IS NOT an extension of ERC-721 or ERC-1155. 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.
Unique and Non-Unique Roles
The standard supports both unique and non-unique roles. Unique roles are roles that can be assigned to only one account, while non-unique roles can be granted to multiple accounts simultaneously. The parameter _supportsMultipleAssignments was included in the hasRole function to support both use cases. When _supportsMultipleAssignments is true, the function checks if the assignment exists and is not expired. However, when false, the function also validates that no other role was granted afterward. In other words, for unique roles, each new assignment invalidates the previous one, and only the last one can be valid.
Assuming that the role was granted and is not expired, the following table shows the result the hasRole function MUST return:
| Role Type | _supportsMultipleAssignments |
Is last Role granted? | Result of hasRole |
|---|---|---|---|
| Non-Unique Role | false |
Irrelevant | true |
| Unique Role | true |
true |
true |
| Unique Role | true |
false |
false |
In conclusion, the _supportsMultipleAssignments argument only affects the result when true, and if the queried assignment is not the last one granted.
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.
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"
},
"supportsMultipleAssignments": {
"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.",
"supportsMultipleAssignments": 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.",
"supportsMultipleAssignments": true,
"inputs": [
{
"name": "rent",
"type": "uint256"
}
]
}
]
}
The properties of the roles array 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 supportsMultipleAssignments field, as shown in the example. This field is used in the hasRole function (refer back to Unique and Non-Unique Roles).
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 and balanceOf aren’t required. Consequently, this design ensures a more straightforward integration with different implementations of NFTs.
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
_grantorto thehasRolefunction. - It’s the responsibility of the dApp to check if the role is unique or non-unique. To ensure the role was not assigned to another account when the role is unique,
hasRoleshould be called with_supportsMultipleAssignmentsset tofalse.
Reference Implementation
NFT Roles - A reference implementation created by Orium Network.
- CC0 License.
- 100% test coverage.