This EIP defines an interface extending EIP-721 to provide shareable multi-privileges for NFTs. Privileges may be on-chain (voting rights, permission to claim an airdrop) or off-chain (a coupon for an online store, a discount at a local restaurant, access to VIP lounges in airports). Each NFT may contain many privileges, and the holder of a privilege can verifiably transfer that privilege to others. Privileges may be non-shareable or shareable. Shareable privileges can be cloned, with the provider able to adjust the details according to the spreading path. Expiration periods can also be set for each privilege.
Motivation
This standard aims to efficiently manage privileges attached to NFTs in real-time. Many NFTs have functions other than just being used as profile pictures or art collections, they may have real utilities in different scenarios. For example, a fashion store may give a discount for its own NFT holders; a DAO member NFT holder can vote for the proposal of how to use their treasury; a dApp may create an airdrop event to attract a certain group of people like some blue chip NFT holders to claim; the grocery store can issue its membership card on chain (as an NFT) and give certain privileges when the members shop at grocery stores, etc. There are cases when people who own NFTs do not necessarily want to use their privileges. By providing additional data recording different privileges a NFT collection has and interfaces to manage them, users can transfer or sell privileges without losing their ownership of the NFT.
EIP-721 only records the ownership and its transfer, the privileges of an NFT are not recorded on-chain. This extension would allow merchants/projects to give out a certain privilege to a specified group of people, and owners of the privileges can manage each one of the privileges independently. This facilitates a great possibility for NFTs to have real usefulness.
For example, an airline company issues a series of EIP-721/EIP-1155 tokens to Crypto Punk holders to give them privileges, in order to attract them to join their club. However, since these tokens are not bound to the original NFT, if the original NFT is transferred, these privileges remain in the hands of the original holders, and the new holders cannot enjoy the privileges automatically.
So, we propose a set of interfaces that can bind the privileges to the underlying NFT, while allowing users to manage the privileges independently.
Specification
The key words “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 complying with this standard MUST implement the IERC5496 interface. The shareable multi-privilege extension is OPTIONAL for EIP-721 contracts.
/// @title multi-privilege extension for EIP-721
/// Note: the EIP-165 identifier for this interface is 0x076e1bbb
interfaceIERC5496{/// @notice Emitted when `owner` changes the `privilege holder` of a NFT.
eventPrivilegeAssigned(uint256tokenId,uint256privilegeId,addressuser,uint256expires);/// @notice Emitted when `contract owner` changes the `total privilege` of the collection
eventPrivilegeTotalChanged(uint256newTotal,uint256oldTotal);/// @notice set the privilege holder of a NFT.
/// @dev expires should be less than 30 days
/// Throws if `msg.sender` is not approved or owner of the tokenId.
/// @param tokenId The NFT to set privilege for
/// @param privilegeId The privilege to set
/// @param user The privilege holder to set
/// @param expires For how long the privilege holder can have
functionsetPrivilege(uint256tokenId,uint256privilegeId,addressuser,uint256expires)external;/// @notice Return the expiry timestamp of a privilege
/// @param tokenId The identifier of the queried NFT
/// @param privilegeId The identifier of the queried privilege
/// @return Whether a user has a certain privilege
functionprivilegeExpires(uint256tokenId,uint256privilegeId)externalviewreturns(uint256);/// @notice Check if a user has a certain privilege
/// @param tokenId The identifier of the queried NFT
/// @param privilegeId The identifier of the queried privilege
/// @param user The address of the queried user
/// @return Whether a user has a certain privilege
functionhasPrivilege(uint256tokenId,uint256privilegeId,addressuser)externalviewreturns(bool);}
Every contract implementing this standard SHOULD set a maximum privilege number before setting any privilege, the privilegeId MUST NOT be greater than the maximum privilege number.
The PrivilegeAssigned event MUST be emitted when setPrivilege is called.
The PrivilegeTotalChanged event MUST be emitted when the total privilege of the collection is changed.
The supportsInterface method MUST return true when called with 0x076e1bbb.
/// @title Cloneable extension - Optional for EIP-721
interfaceIERC721Cloneable{/// @notice Emitted when set the `privilege ` of a NFT cloneable.
eventPrivilegeCloned(uinttokenId,uintprivId,addressfrom,addressto);/// @notice set a certain privilege cloneable
/// @param tokenId The identifier of the queried NFT
/// @param privilegeId The identifier of the queried privilege
/// @param referrer The address of the referrer
/// @return Whether the operation is successful or not
functionclonePrivilege(uinttokenId,uintprivId,addressreferrer)externalreturns(bool);}
The PrivilegeCloned event MUST be emitted when clonePrivilege is called.
For Compliant contract, it is RECOMMENDED to use EIP-1271 to validate the signatures.
Rationale
Shareable Privileges
The number of privilege holders is limited by the number of NFTs if privileges are non-shareable. A shareable privilege means the original privilege holder can copy the privilege and give it to others, not transferring his/her own privilege to them. This mechanism greatly enhances the spread of privileges as well as the adoption of NFTs.
Expire Date Type
The expiry timestamp of a privilege is a timestamp and stored in uint256 typed variables.
Beneficiary of Referrer
For example, a local pizza shop offers a 30% off Coupon and the owner of the shop encourages their consumers to share the coupon with friends, then the friends can get the coupon. Let’s say Tom gets 30% off Coupon from the shop and he shares the coupon with Alice. Alice gets the coupon too and Alice’s referrer is Tom. For some certain cases, Tom may get more rewards from the shop. This will help the merchants in spreading the promotion among consumers.
Proposal: NFT Transfer
If the owner of the NFT transfers ownership to another user, there is no impact on “privileges”. But errors may occur if the owner tries to withdraw the original EIP-721 token from the wrapped NFT through unwrap() if any available privileges are still ongoing. We protect the rights of holders of the privileges to check the last expiration date of the privilege.
functionunwrap(uint256tokenId,addressto)external{require(getBlockTimestamp()>=privilegeBook[tokenId].lastExpiresAt,"privilege not yet expired");require(ownerOf(tokenId)==msg.sender,"not owner");_burn(tokenId);IERC721(nft).transferFrom(address(this),to,tokenId);emitUnwrap(nft,tokenId,msg.sender,to);}
Backwards Compatibility
This EIP is compatible with any kind of NFTs that follow the EIP-721 standard. It only adds more functions and data structures without interfering with the original EIP-721 standard.
Test Cases
Test cases are implemented with the reference implementation.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"@openzeppelin/contracts/utils/introspection/IERC165.sol";import"./IERC5496.sol";contractERC5496isERC721,IERC5496{structPrivilegeRecord{addressuser;uint256expiresAt;}structPrivilegeStorage{uintlastExpiresAt;// privId => PrivilegeRecord
mapping(uint=>PrivilegeRecord)privilegeEntry;}uintpublicprivilegeTotal;// tokenId => PrivilegeStorage
mapping(uint=>PrivilegeStorage)publicprivilegeBook;mapping(address=>mapping(address=>bool))privateprivilegeDelegator;constructor(stringmemoryname_,stringmemorysymbol_)ERC721(name_,symbol_){}functionsetPrivilege(uinttokenId,uintprivId,addressuser,uint64expires)externalvirtual{require((hasPrivilege(tokenId,privId,ownerOf(tokenId))&&_isApprovedOrOwner(msg.sender,tokenId))||_isDelegatorOrHolder(msg.sender,tokenId,privId),"ERC721: transfer caller is not owner nor approved");require(expires<block.timestamp+30days,"expire time invalid");require(privId<privilegeTotal,"invalid privilege id");privilegeBook[tokenId].privilegeEntry[privId].user=user;if(_isApprovedOrOwner(msg.sender,tokenId)){privilegeBook[tokenId].privilegeEntry[privId].expiresAt=expires;if(privilegeBook[tokenId].lastExpiresAt<expires){privilegeBook[tokenId].lastExpiresAt=expires;}}emitPrivilegeAssigned(tokenId,privId,user,uint64(privilegeBook[tokenId].privilegeEntry[privId].expiresAt));}functionhasPrivilege(uint256tokenId,uint256privId,addressuser)publicvirtualviewreturns(bool){if(privilegeBook[tokenId].privilegeEntry[privId].expiresAt>=block.timestamp){returnprivilegeBook[tokenId].privilegeEntry[privId].user==user;}returnownerOf(tokenId)==user;}functionprivilegeExpires(uint256tokenId,uint256privId)publicvirtualviewreturns(uint256){returnprivilegeBook[tokenId].privilegeEntry[privId].expiresAt;}function_setPrivilegeTotal(uinttotal)internal{emitPrivilegeTotalChanged(total,privilegeTotal);privilegeTotal=total;}functiongetPrivilegeInfo(uinttokenId,uintprivId)externalviewreturns(addressuser,uint256expiresAt){return(privilegeBook[tokenId].privilegeEntry[privId].user,privilegeBook[tokenId].privilegeEntry[privId].expiresAt);}functionsetDelegator(addressdelegator,boolenabled)external{privilegeDelegator[msg.sender][delegator]=enabled;}function_isDelegatorOrHolder(addressdelegator,uint256tokenId,uintprivId)internalvirtualviewreturns(bool){addressholder=privilegeBook[tokenId].privilegeEntry[privId].user;return(delegator==holder||isApprovedForAll(holder,delegator)||privilegeDelegator[holder][delegator]);}functionsupportsInterface(bytes4interfaceId)publicoverridevirtualviewreturns(bool){returninterfaceId==type(IERC5496).interfaceId||super.supportsInterface(interfaceId);}}
Security Considerations
Implementations must thoroughly consider who has the permission to set or clone privileges.