EIP4400 - ERC-721 Consumable Extension
# Abstract
This specification defines standard functions outlining a consumer
role for instance(s)
of ERC-721. An implementation allows reading the current consumer
for a given NFT (tokenId
) along
with a standardized event for when an consumer
has changed. The proposal depends on and extends the
existing ERC-721.
# Motivation
Many ERC-721 contracts introduce their own custom role that grants permissions for utilising/consuming a
given NFT instance. The need for that role stems from the fact that other than owning the NFT instance, there are other
actions that can be performed on an NFT. For example, various metaverses use operator
/contributor
roles for Land (ERC-721), so that owners of the land can authorise other addresses to deploy scenes to them (f.e.
commissioning a service company to develop a scene).
It is common for NFTs to have utility other than simply owning it. That being said, it requires a separate standardized consumer role, allowing compatibility with user interfaces and contracts, managing those contracts.
Having a consumer
role will enable protocols to integrate and build on top of dApps that issue ERC-721 tokens.
Example of kinds of contracts and applications that can benefit from this standard are predominantly metaverses that have land and other types of digital assets in those metaverses (scene deployment on land, renting land/characters/clothes/passes to events etc.)
# Specification
The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Every contract compliant to the ERC721Consumable
extension MUST implement the IERC721Consumable
interface. The **
consumer extension** is OPTIONAL for ERC-721 contracts.
/// @title ERC-721 Consumer Role extension
/// Note: the ERC-165 identifier for this interface is 0x953c8dfa
interface IERC721Consumable /* is ERC721 */ {
/// @notice Emitted when `owner` changes the `consumer` of an NFT
/// The zero address for consumer indicates that there is no consumer address
/// When a Transfer event emits, this also indicates that the consumer address
/// for that NFT (if any) is set to none
event ConsumerChanged(address indexed owner, address indexed consumer, uint256 indexed tokenId);
/// @notice Get the consumer address of an NFT
/// @dev The zero address indicates that there is no consumer
/// Throws if `_tokenId` is not a valid NFT
/// @param _tokenId The NFT to get the consumer address for
/// @return The consumer address for this NFT, or the zero address if there is none
function consumerOf(uint256 _tokenId) view external returns (address);
/// @notice Change or reaffirm the consumer address for an NFT
/// @dev The zero address indicates there is no consumer address
/// Throws unless `msg.sender` is the current NFT owner, an authorised
/// operator of the current owner or approved address
/// Throws if `_tokenId` is not valid NFT
/// @param _consumer The new consumer of the NFT
function changeConsumer(address _consumer, uint256 _tokenId) external;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Every contract implementing the ERC721Consumable
extension is free to define the permissions of a consumer
(e.g.
what are consumers allowed to do within their system) with only one exception - consumers MUST NOT be considered owners,
authorised operators or approved addresses as per the ERC-721 specification. Thus, they MUST NOT be able to execute
transfers & approvals.
The consumerOf(uint256 _tokenId)
function MAY be implemented as pure
or view
.
The changeConsumer(address _consumer, uint256 _tokenId)
function MAY be implemented as public
or external
.
The ConsumerChanged
event MUST be emitted when a consumer is changed.
On every transfer
, the consumer MUST be changed to the zero address the same way approve
address is changed.
The supportsInterface
method MUST return true
when called with 0x953c8dfa
.
# Rationale
Key factors influencing the standard:
- Keeping the number of functions in the interfaces to a minimum to prevent contract bloat.
- Simplicity
- Gas Efficiency
- Not reusing or overloading other already existing roles (e.g. owners, operators, approved addresses)
# Name
The chosen name resonates with the purpose of its existence. Consumers can be considered entities that utilise the token instances, without necessarily having ownership rights to it.
The other name for the role that was considered was operator
, however it is already defined and used within
the ERC-721
standard.
# Restriction on the Permissions
There are numerous use-cases where a distinct role for NFTs is required that MUST NOT have owner permissions. A contract that implements the consumer role and grants ownership permissions to the consumer renders this standard pointless.
# Backwards Compatibility
There are no other standards that define a similar role for NFTs and the name (consumer
) is not used by other ERC-721
related standards.
# Test Cases
Test cases are implemented in the reference implementation repository here (opens new window)
# Reference Implementation
The following is a snippet for reference implementation of the ERC721Consumer
extension. The full repository can be
found here (opens new window)
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC721Consumable.sol";
contract ERC721Consumable is IERC721Consumable, ERC721 {
// Mapping from token ID to consumer address
mapping (uint256 => address) _tokenConsumers;
constructor() ERC721("ReferenceImpl", "RIMPL") {
}
/**
* @dev See {IERC721Consumable-consumerOf}
*/
function consumerOf(uint256 _tokenId) view external returns (address) {
require(_exists(_tokenId), "ERC721Consumable: consumer query for nonexistent token");
return _tokenConsumers[_tokenId];
}
/**
* @dev See {IERC721Consumable-changeConsumer}
*/
function changeConsumer(address _consumer, uint256 _tokenId) external {
address owner = this.ownerOf(_tokenId);
require(msg.sender == owner || msg.sender == getApproved(_tokenId) ||
isApprovedForAll(owner, msg.sender),
"ERC721Consumable: changeConsumer caller is not owner nor approved");
_changeConsumer(owner, _consumer, _tokenId);
}
/**
* @dev Changes the consumer
* Requirement: `tokenId` must exist
*/
function _changeConsumer(address _owner, address _consumer, uint256 _tokenId) internal {
_tokenConsumers[_tokenId] = _consumer;
emit ConsumerChanged(_owner, _consumer, _tokenId);
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
return interfaceId == type(IERC721Consumable).interfaceId || super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(address _from, address _to, uint256 _tokenId) internal virtual override (ERC721) {
super._beforeTokenTransfer(_from, _to, _tokenId);
_changeConsumer(_from, address(0), _tokenId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# Security Considerations
Implementors of the ERC721Consumable
standard must consider thoroughly the permissions they give to consumers
. Even
if they implement the standard correctly and do not allow transfer/burning of NFTs, they might still provide permissions
to the consumers
that they might not want to provide otherwise and should be restricted to owners
only.
# Copyright
Copyright and related rights waived via CC0 (opens new window).