EIP 5496: Multi-privilege Management Extension for ERC-721

eip: 5496

title: ERC-721 Multi-privilege Management Extension

description: Standard interface for creating sharable multi-privilege NFTs for ERC-721

author: wnft dev@wnft.one

discussions-to:

status: Draft

type: Standards Track

category: ERC

created: 2022-7-30

requires: 721


Abstract

This specification defines standard functions that extends current [ERC-721] standard providing sharable multiple privileges for a NFT collection. The privileges could be the 1) on-chain ones, such as voting rights, or to claim a targeted airdrop; 2) or off-chain ones, such as a coupon in Amazon, or a discount in a local New York restaurant, or even a VIP lounge access in the airport. There could be multiple privileges attached to one NFT and each privilege holder of an NFT can transfer the privilege to others. The ownership of the privileges is verifiable. On top of the multiple-privilege extension, this standard also contains methods to give the provider to specify the privilege non-sharable or sharable. Sharable privileges can be done by simply cloning a privilege, furthermore, the provider would be able to adjust the details of privileges according to the spreading path. Expiration period can also be set to each privilege. The proposal depends on and extends the existing [ERC-721].

You can find a draft of the system here: GitHub - ERC721-P: A contract to manage sharable multi-privilege NFTs

Motivation

Many NFTs have functions other than just being used as profile pictures or collection of art works, 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; some dApp may create an airdrop event to attract a certain group of people like some bluechip NFT holders to claim; Walmart can issue its membership card on chain(NFT) and give certain privileges when the members shop at the Walmart stores, etc. There are cases when people who own NFTs do not necessarily want to / not being able to use privileges. By providing additional data recording different privileges a NFT collection has and interfaces to manage them, users can now transfer or sell privileges without losing their ownership of the NFT.

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 ERC721P extension MUST implement the IERC721P interface. The **

sharable multi-privilege extension** is OPTIONAL for ERC-721 contracts.


/// @title ERC-721P multi-privilege extension

///  Note: the ERC-165 identifier for this interface is 0x953c8dfa

interface IERC721P /* is ERC721 */ {

    /// @notice Emitted when `owner` changes the `privilege holder` of a NFT.

    event PrivilegeAssigned(uint256 tokenId, uint256 privilegeId, address user, uint256 expires);

    /// @notice Emitted when `privilege holder` changes the `holder` of a privilege

    event PrivilegeTransfered(uint256 tokenId, uint256 privilegeId, address from, address to);

    /// @notice Emitted when `contract owner` changes the `total privilege` of the collection

    event PrivilegeTotalChanged(uint256 newTotal, uint256 oldTotal);

    /// @notice set the privilege holder of a NFT.

    /// @dev expires should better 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

    function setPrivilege(uint256 tokenId, uint256 privilegeId, address user, uint256 expires) external;    

    /// @notice Check if a privilege has expired

    /// @param tokenId The identifier of the queried NFT

    /// @param privilegeId The identifier of the queried privilege

    /// @return Whether a user has a certain privilege

    function privilegeExpires(uint256 tokenId, uint256 privilegeId) external view returns(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

    function hasPrivilege(uint256 tokenId, uint256 privilegeId, address user) external view returns(bool);

}

Every contract implementing the ERC721P 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 PrivilegeTransfered event MUST be emitted when transferPrivilege is called.

The supportsInterface method MUST return true when called with 0x076e1bbb.


/// @title ERC-721P Cloneable extension - Optional

interface IERC721PCloneable {

    /// @notice Emitted when set the `privilege ` of a NFT cloneable.

    event PrivilegeCloned(uint tokenId, uint privId, address from, address to);

    /// @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 sucessful or not

    function clonePrivilege(uint tokenId, uint privId, address referrer) external returns (bool);

}

The PrivilegeCloned event MUST be emitted when clonePrivilege is called.

Rationale

Multiple privileges

Current ERC-721 standard only record 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.

Sharable privileges

The number of privilege holders is limited by the number of NFTs if privileges are non-sharable. A sharable privilege means the original privilege holder can copy the privilege and give it to the others, not transferring his/her own privilege to the others. This mechanism greatly enhances the spread of privileges as well as the adoption of NFTs.

Backwards Compatibility

ERC721PA is compatible to any kind of NFTs that follow ERC-721 standard. It only adds more functions and data structures without interfering with original ERC-721 standard.

Test Cases

Test cases are implemented in the reference implementation

repository.

Reference Implementation

The following is a snippet for reference implementation of the ERC721P extension. The full repository can be

found in github.


// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

import "./IERC721P.sol";

contract ERC721P is ERC721, IERC721P {

    struct PrivilegeRecord {

        address user;

        uint256 expiresAt;

    }

    struct PrivilegeStorage {

        uint lastExpiresAt;

        // privId => PrivilegeRecord

        mapping(uint => PrivilegeRecord) privilegeEntry;

    }

    uint public privilegeTotal;

    // tokenId => PrivilegeStorage

    mapping(uint => PrivilegeStorage) public privilegeBook;

    mapping(address => mapping(address => bool)) private privilegeDelegator;

    constructor(string memory name_, string memory symbol_)

    ERC721(name_,symbol_)

    {

   

    }

    function setPrivilege(

        uint tokenId,

        uint privId,

        address user,

        uint64 expires

    ) external virtual {

        require(_isApprovedOrOwner(msg.sender, tokenId) || _isDelegatorOrHolder(msg.sender, tokenId, privId), "ERC721: transfer caller is not owner nor approved");

        require(expires < block.timestamp + 30 days, "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;

            }

        }

        emit PrivilegeAssigned(tokenId, privId, user, uint64(privilegeBook[tokenId].privilegeEntry[privId].expiresAt));

    }

    function hasPrivilege(

        uint256 tokenId,

        uint256 privId,

        address user

    ) public virtual view returns(bool) {

        if ( privilegeBook[tokenId].privilegeEntry[privId].expiresAt >=  block.timestamp ){

            return privilegeBook[tokenId].privilegeEntry[privId].user == user;

        }

        return ownerOf(tokenId) == user;

    }

    function privilegeExpires(

        uint256 tokenId,

        uint256 privId

    ) public virtual view returns(uint256){

        return privilegeBook[tokenId].privilegeEntry[privId].expiresAt;

    }

    function _setPrivilegeTotal(

        uint total

    ) internal {

        emit PrivilegeTotalChanged(total, privilegeTotal);

        privilegeTotal = total;

    }

    function getPrivilegeInfo(uint tokenId, uint privId) external view returns(address user, uint256 expiresAt) {

        return (privilegeBook[tokenId].privilegeEntry[privId].user, privilegeBook[tokenId].privilegeEntry[privId].expiresAt);

    }

    function setDelegator(address delegator, bool enabled) external {

        privilegeDelegator[msg.sender][delegator] = enabled;

    }

    function _isDelegatorOrHolder(address delegator, uint256 tokenId, uint privId) internal virtual view returns (bool) {

        address holder = privilegeBook[tokenId].privilegeEntry[privId].user;

         return (delegator == holder || isApprovedForAll(holder, delegator) || privilegeDelegator[holder][delegator]);

    }

    function supportsInterface(bytes4 interfaceId) public override virtual view returns (bool) {

        return interfaceId == type(IERC721P).interfaceId || super.supportsInterface(interfaceId);

    }

}

Security Considerations

Implementors of the ERC721P standard must consider thoroughly who has the permission to set or clone privileges.

Copyright

Copyright and related rights waived via [CC0].