📖 This EIP is in the review stage. It is subject to changes and feedback is appreciated.

EIP-5496: Multi-privilege Management NFT Extension Source

Create shareable multi-privilege NFTs for EIP-721

AuthorJeremy Z
Discussions-Tohttps://ethereum-magicians.org/t/eip-5496-multi-privilege-management-extension-for-erc-721/10427
StatusReview
TypeStandards Track
CategoryERC
Created2022-7-30
Requires 721

Abstract

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

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; Walmart can issue its membership card on chain (as an NFT) and give certain privileges when the members shop at Walmart 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.

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 compliant to the ERC5496 extension 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 ERC-165 identifier for this interface is 0x953c8dfa
interface IERC5496 /* 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 ERC5496 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 Cloneable extension - Optional for ERC-721
interface IERC721Cloneable {
    /// @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 successful 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

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.

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 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

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.

Test Code

test.js

Run in terminal:

truffle test ./test/test.js

testCloneable.js

Run in terminal:

truffle test ./test/testCloneable.js

Reference Implementation

// 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 "./IERC5496.sol";

contract ERC5496 is ERC721, IERC5496 {
    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(IERC5496).interfaceId || super.supportsInterface(interfaceId);
    }
}

Security Considerations

Implementations must thoroughly consider who has the permission to set or clone privileges.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Jeremy Z, "EIP-5496: Multi-privilege Management NFT Extension [DRAFT]," Ethereum Improvement Proposals, no. 5496, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5496.