ERC-4907 NFT rental extension (user and expiry) (Rental NFT, an Extension of EIP-721)
Final · NFTs & multi-token · created 2022-03-11
Adds a separate "user" role and an expiry time (expires) apart from the NFT's owner. It lets an NFT be lent for a set period without transferring ownership.
Why it is needed
Some NFTs have a practical use, such as building a scene on virtual land or using a game item inside a game. For these NFTs, the owner and the person who actually uses the NFT can differ. A typical case is an owner lending it to someone else. However, the borrower should not have the same rights as the owner. Normally a borrower must not be able to sell the NFT. The existing NFT standard (EIP-721) has no role that separates owner from user, so each project built its own under names like "operator" or "controller". Because names and behavior differ, it is hard for apps to work together. Also, time-limited use such as a rental requires rights to be withdrawn when the period ends. Without an expiry feature, the owner must send two transactions to the chain: one to register the user at the start and one to withdraw at the end. This takes more effort and doubles the gas cost.
What to implement
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);: Logs when the user of an NFT or the user's expiry time changes. If user is the zero address, there is no user.function setUser(uint256 tokenId, address user, uint64 expires) external;: Sets the user and expiry time of an NFT. Passing the zero address leaves the NFT with no user, and an invalid tokenId makes the call fail.function userOf(uint256 tokenId) external view returns(address);: Returns the current user address of the NFT. Returns the zero address if there is no user or the user has expired.function userExpires(uint256 tokenId) external view returns(uint256);: Returns the user's expiry time. A value of 0 means there is no user.
In depth
Separating the two roles of owner and user
This standard extends EIP-721 by adding a user role and an expires value. The owner is the owner from the existing EIP-721, and the user is an address with only the right to 'use' the NFT. The user cannot transfer the NFT or assign another user. The original text explains that this separation makes it easier to manage what the lending side and the borrowing side can each do, that is, each party's rights. The owner can set the user, and other projects can attach their own service permissions to the owner and the user separately. Existing EIP-721 functions are left as they are and only a set of functions is added, so the standard is fully compatible with EIP-721.
setUser: setting the user and expiry time
setUser(uint256 tokenId, address user, uint64 expires) sets the NFT's user and expires in one call. Passing the zero address as user leaves the NFT with no user. If tokenId is not a valid NFT, the call must fail (throw). expires is a UNIX timestamp, and the new user can use the NFT until that time. The function MAY be implemented as either public or external. Who can call it is not specified in the interface, and the reference implementation allows only the owner or an approved address through _isApprovedOrOwner(msg.sender, tokenId). The reference implementation stores UserInfo{user, expires} in _users[tokenId] and then emits UpdateUser.
userOf and userExpires: lookup rules
userOf(uint256 tokenId) returns the NFT's user address. It returns the zero address if there is no user or the user has expired. Callers do not need to compute expiry themselves and can rely on the userOf result alone. userExpires(uint256 tokenId) returns the expiry time. A value of 0 means there is no user. Both functions MAY be implemented as pure or view. In the reference implementation, userOf returns the stored user if uint256(_users[tokenId].expires) >= block.timestamp, and address(0) otherwise. If the expiry time equals the current block time, the user is still considered valid. The reference implementation's userExpires returns the stored expires as is, so a nonzero value can appear even after expiry has passed.
UpdateUser event
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires) MUST be emitted whenever user or expires changes. If user is the zero address, there is no user. Because tokenId and user are indexed, logs can be filtered by a specific NFT or a specific user. The reference implementation emits this event when setUser is called and when the NFT is transferred to another address and the user information is cleared.
ERC-165 interface identifier
Calling supportsInterface with 0xad092b5c MUST return true. This value is the identifier of the IERC4907 interface. External contracts such as rental platforms can use this call to first check whether an NFT supports user and expires. The reference implementation uses interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId), so it also supports the EIP-721 identifiers. This standard requires EIP-165 and EIP-721 (requires: 165, 721).
Transfer handling and tests in the reference implementation
The reference implementation ERC4907.sol inherits OpenZeppelin v4 ERC721 and overrides _beforeTokenTransfer. If from != to and _users[tokenId].user is not the zero address, it clears the user information with delete _users[tokenId] and emits UpdateUser(tokenId, address(0), 0). In other words, when the NFT moves to another address, the existing rental relationship disappears. This behavior lives in the reference implementation, not in the interface specification, so other implementations do not have to work the same way. The test code mints token 1 to Alice, calls setUser for Bob with an expiry of the current time + 1000 seconds, and checks that userOf(1) is Bob and ownerOf(1) is Alice.
Permissionless third-party integration
The original text explains that this standard aims at permissionless interoperability. If an NFT project adopts the user role and expires, other projects can access this feature directly and build their own kinds of transactions without the permission of the NFT issuer or the original app. The examples the original text gives are connecting the same PFP NFT at the same time to a rental platform that lends it out for 30 days and to a collateral platform where a buyer pays in installments, uses the NFT, and eventually gains ownership. Neither case needs permission from the original PFP project. It also explains that the new functions look similar to the existing EIP-721 functions, so developers can adopt them quickly.
Things to watch out for
- The owner can change the user and expiry time at any time: The original text explains that this standard fully protects the owner's rights. The owner can change the NFT's user and expires at any time. In other words, even before the rental period ends, the owner can replace or remove the user.