Appearance
Abstract
The following standard proposes an additional user
role for EIP-721. This role grants the permission to use the NFT with no ability to transfer or set users. It has an expiry and a flag if the token is borrowed or not. Owner
can delegate the NFT for usage to hot wallets or lend the NFT. If the token is borrowed, not even the owner can change the user until the status expires or both parties agree to terminate. This way, it is possible to keep both roles active at the same time.
Motivation
Collectibles, gaming assets, metaverse, event tickets, music, video, domains, real item representation are several among many NFT use cases. With EIP-721 only the owner can reap the benefits. However, with most of the utilities it would be beneficial to distinguish between the token owner and its user. For instance music or movies could be rented. Metaverse lands could be delegated for usage.
The two reasons why to set the user are:
- delegation - Assign user to your hot wallet to interact with applications securely. In this case, the owner can change the user at any time.
- renting - This use case comes with additional requirements. It is needed to terminate the loan once the established lending period is over. This is provided by
expires
of the user. It is also necessary to protect the borrower against resetting their status by the owner. Thus,isBorrowed
check must be implemented to disable the option to set the user before the contract expires.
The most common use cases for having an additional user role are:
- delegation - For security reasons.
- gaming - Would you like to try a game (or particular gaming assets) but are you unsure whether or not you will like it? Rent assets first.
- guilds - Keep the owner of the NFTs as the multisig wallet and set the user to a hot wallet with shared private keys among your guild members.
- events - Distinguish between
ownerOf
anduserOf
. Each role has a different access. - social - Differentiate between roles for different rooms. For example owner has read + write access while userOf has read access only.
This proposal is a follow up on EIP-4400 and EIP-4907 and introduces additional upgrades for lending and borrowing which include:
- NFT stays in owner's wallet during rental period
- Listing and sale of NFT without termination of the rent
- Claiming owner benefits during rental period
Building the standard with additional isBorrowed check now allows to create rental marketplaces which can set the user of NFT without the necessary staking mechanism. With current standards if a token is not staked during the rental period, the owner can simply terminate the loan by setting the user repeatedly. This is taken care of by disabling the function if the token is borrowed which in turn is providing the owner additional benefits. They can keep the token tied to their wallet, meaning they can still receive airdrops, claim free mints based on token ownership or otherwise use the NFT provided by third-party services for owners. They can also keep the NFT listed for sale. Receiving airdrops or free mints was previously possible but the owner was completely reliant on the implementation of rental marketplaces and their discretion.
Decentralized applications can now differentiate between ownerOf and userOf while both statuses can coexist.
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 compliant contract MUST implement the IERC5501
interface. This extension is OPTIONAL for EIP-721 contracts.
solidity
/**
* @title IERC5501: Rental & Delegation NFT - EIP-721 Extension
* @notice the EIP-165 identifier for this interface is 0xf808ec37.
*/
interface IERC5501 /* is IERC721 */ {
/**
* @dev Emitted when the user of an NFT is modified.
*/
event UpdateUser(uint256 indexed _tokenId, address indexed _user, uint64 _expires, bool _isBorrowed);
/**
* @notice Set the user info of an NFT.
* @dev User address cannot be zero address.
* Only approved operator or NFT owner can set the user.
* If NFT is borrowed, the user info cannot be changed until user status expires.
* @param _tokenId uint256 ID of the token to set user info for
* @param _user address of the new user
* @param _expires Unix timestamp when user info expires
* @param _isBorrowed flag whether or not the NFT is borrowed
*/
function setUser(uint256 _tokenId, address _user, uint64 _expires, bool _isBorrowed) external;
/**
* @notice Get the user address of an NFT.
* @dev Reverts if user is not set.
* @param _tokenId uint256 ID of the token to get the user address for
* @return address user address for this NFT
*/
function userOf(uint256 _tokenId) external view returns (address);
/**
* @notice Get the user expires of an NFT.
* @param _tokenId uint256 ID of the token to get the user expires for
* @return uint64 user expires for this NFT
*/
function userExpires(uint256 _tokenId) external view returns (uint64);
/**
* @notice Get the user isBorrowed of an NFT.
* @param _tokenId uint256 ID of the token to get the user isBorrowed for
* @return bool user isBorrowed for this NFT
*/
function userIsBorrowed(uint256 _tokenId) external view returns (bool);
}
Every contract implementing the IERC5501
interface is free to define the permissions of a user
. However, user MUST NOT be considered an owner
. They MUST NOT be able to execute transfers and approvals. Furthermore, setUser
MUST be blocked from executing if userIsBorrowed
returns true
and userExpires
is larger than or equal to block.timestamp
.
The UpdateUser
event MUST be emitted when a user
is changed.
The setUser(uint256 _tokenId, address _user, uint64 _expires, bool _isBorrowed)
function SHOULD revert
unless the msg.sender
is the owner
or an approved operator. It MUST revert if a token is borrowed and status has not expired yet. It MAY be public
or external
.
The userOf(uint256 _tokenId)
function SHOULD revert if user
is not set or expired.
The userExpires(uint256 _tokenId)
function returns a timestamp when user status expires.
The userIsBorrowed(uint256 _tokenId)
function returns whether NFT is borrowed or not.
The supportsInterface
function MUST return true
when called with 0xf808ec37
.
On every transfer
, the user
MUST be reset if the token is not borrowed. If the token is borrowed the user
MUST stay the same.
The Balance extension is OPTIONAL. This gives the option to query the number of tokens a user
has.
solidity
/**
* @title IERC5501Balance
* Extension for ERC5501 which adds userBalanceOf to query how many tokens address is userOf.
* @notice the EIP-165 identifier for this interface is 0x0cb22289.
*/
interface IERC5501Balance /* is IERC5501 */{
/**
* @notice Count of all NFTs assigned to a user.
* @dev Reverts if user is zero address.
* @param _user an address for which to query the balance
* @return uint256 the number of NFTs the user has
*/
function userBalanceOf(address _user) external view returns (uint256);
}
The userBalanceOf(address _user)
function SHOULD revert
for zero address.
The Enumerable extension is OPTIONAL. This allows to iterate over user balance.
solidity
/**
* @title IERC5501Enumerable
* This extension for ERC5501 adds the option to iterate over user tokens.
* @notice the EIP-165 identifier for this interface is 0x1d350ef8.
*/
interface IERC5501Enumerable /* is IERC5501Balance, IERC5501 */ {
/**
* @notice Enumerate NFTs assigned to a user.
* @dev Reverts if user is zero address or _index >= userBalanceOf(_owner).
* @param _user an address to iterate over its tokens
* @return uint256 the token ID for given index assigned to _user
*/
function tokenOfUserByIndex(address _user, uint256 _index) external view returns (uint256);
}
The tokenOfUserByIndex(address _user, uint256 _index)
function SHOULD revert
for zero address and throw
if the index is larger than or equal to user
balance.
The Terminable extension is OPTIONAL. This allows terminating the rent early if both parties agree.
solidity
/**
* @title IERC5501Terminable
* This extension for ERC5501 adds the option to terminate borrowing if both parties agree.
* @notice the EIP-165 identifier for this interface is 0x6a26417e.
*/
interface IERC5501Terminable /* is IERC5501 */ {
/**
* @dev Emitted when one party from borrowing contract approves termination of agreement.
* @param _isLender true for lender, false for borrower
*/
event AgreeToTerminateBorrow(uint256 indexed _tokenId, address indexed _party, bool _isLender);
/**
* @dev Emitted when agreements to terminate borrow are reset.
*/
event ResetTerminationAgreements(uint256 indexed _tokenId);
/**
* @dev Emitted when borrow of token ID is terminated.
*/
event TerminateBorrow(uint256 indexed _tokenId, address indexed _lender, address indexed _borrower, address _caller);
/**
* @notice Agree to terminate a borrowing.
* @dev Lender must be ownerOf token ID. Borrower must be userOf token ID.
* If lender and borrower are the same, set termination agreement for both at once.
* @param _tokenId uint256 ID of the token to set termination info for
*/
function setBorrowTermination(uint256 _tokenId) external;
/**
* @notice Get if it is possible to terminate a borrow agreement.
* @param _tokenId uint256 ID of the token to get termination info for
* @return bool, bool first indicates lender agrees, second indicates borrower agrees
*/
function getBorrowTermination(uint256 _tokenId) external view returns (bool, bool);
/**
* @notice Terminate a borrow if both parties agreed.
* @dev Both parties must have agreed, otherwise revert.
* @param _tokenId uint256 ID of the token to terminate borrow of
*/
function terminateBorrow(uint256 _tokenId) external;
}
The AgreeToTerminateBorrow
event MUST be emitted when either the lender or borrower agrees to terminate the rent.
The ResetTerminationAgreements
event MUST be emitted when a token is borrowed and transferred or setUser
and terminateBorrow
functions are called.
The TerminateBorrow
event MUST be emitted when the rent is terminated.
The setBorrowTermination(uint256 _tokenId)
. It MUST set an agreement from either party whichever calls the function. If the lender and borrower are the same address, it MUST assign an agreement for both parties at once.
The getBorrowTermination(uint256 _tokenId)
returns if agreements from both parties are true
or false
.
The terminateBorrow(uint256 _tokenId)
function MAY be called by anyone. It MUST revert
if both agreements to terminate are not true
. This function SHOULD change the isBorrowed
flag from true
to false
.
On every transfer
, the termination agreements from either party MUST be reset if the token is borrowed.
Rationale
The main factors influencing this standard are:
- EIP-4400 and EIP-4907
- Allow lending and borrowing without the necessary stake or overcollateralization while owner retains ownership
- Leave the delegation option available
- Keep the number of functions in the interfaces to a minimum while achieving desired functionality
- Modularize additional extensions to let developers choose what they need for their project
Name
The name for the additional role has been chosen to fit the purpose and to keep compatibility with EIP-4907.
Ownership retention
Many collections offer their owners airdrops or free minting of various tokens. This is essentially broken if the owner is lending a token by staking it into a contract (unless the contract is implementing a way to claim at least airdropped tokens). Applications can also provide different access and benefits to owner and user roles in their ecosystem.
Balance and Enumerable extensions
These have been chosen as OPTIONAL extensions due to the complexity of implementation based on the fact that balance is less once user status expires and there is no immediate on-chain transaction to evaluate that. In both userBalanceOf
and tokenOfUserByIndex
functions there must be a way to determine whether or not user status has expired.
Terminable extension
If the owner mistakenly sets a user with borrow status and expires to a large value they would essentially be blocked from setting the user ever again. The problem is addressed by this extension if both parties agree to terminate the user status.
Security
Once applications adopt the user role, it is possible to delegate ownership to hot wallet and interact with them with no fear of connecting to malicious websites.
Backwards Compatibility
This standard is compatible with current EIP-721 by adding an extension function set. The new functions introduced are similar to existing functions in EIP-721 which guarantees easy adoption by developers and applications. This standard also shares similarities to EIP-4907 considering user role and its expiry which means applications will be able to determine the user if either of the standards is used.
Test Cases
Test cases can be found in the reference implementation:
- Main contract
- Balance extension
- Enumerable extension
- Terminable extension
- Scenario combined of all extensions
Reference Implementation
The reference implementation is available here:
- Main contract
- Balance extension
- Enumerable extension
- Terminable extension
- Solution combined of all extensions
Security Considerations
Developers implementing this standard and applications must consider all the permissions they give to users and owners. Since owner and user are both active roles at the same time, double-spending problem must be avoided. Balance extension must be implemented in such a way which will not cause any gas problems. Marketplaces should let users know if a token listed for sale is borrowed or not.
Copyright
Copyright and related rights waived via CC0.