What is NFTfi? (ERC-4907)
"Since the emergence of NFTs in 2022, numerous NFT projects have appeared and a tremendous amount of NFTs have been traded. However, in contrast to this popularity, questions about the effectiveness of NFTs have been constantly raised. This is because, despite spending a lot of money to purchase and own an NFT, there was no way to use it. To solve this problem, NFT Service Providers have expanded the usability of NFTs by offering various benefits such as service discounts and event participation to users who own their NFTs. Now, they have begun to build an ecosystem where NFTs can be used financially. This ecosystem is called NFTfi. Let's take a look at what NFTfi is and how one of the contracts related to it, ERC-4907, is written.”
NFTfi
NFTfi is a term that combines NFT and Decentralized Finance (DeFi), and it refers to an ecosystem where you can secure the value and liquidity of NFTs by lending tokens with NFT as collateral or renting NFTs so that buyers can use them as needed. In the case of traditional NFTs, there were not many ways for the owner to use the NFT, but with the activation of NFTfi, it expanded the usability of NFTs, increased their value, supplied liquidity based on this, and opened a new path in the NFT market. The ways to use NFT in NFTfi are 1. Loan with NFT, 2. NFT Staking, 3. NFT Rental. The representative projects and processes for these three methods are as follows.
NFT loan
- The borrower presents an NFT to be used as collateral.
- The lender evaluates the value of the NFT and proposes a loan amount, repayment amount, and repayment date. The loan amount is the amount that the borrower borrows from the lender, and the repayment amount is the amount that the borrower has to repay to the lender.
- Once the contract is concluded, the NFT is locked and the borrower receives the loan amount. After this, there are two options.
NFT Staking
- The owner locks up their NFT using a staking service.
- The NFT is deposited and the owner obtains the right to participate in staking. By participating in staking, the owner can earn profits.
NFT Rental
- The owner proposes an amount and expiration time to rent out their NFT.
- The user sends the amount proposed by the owner and receives the right to use the NFT. (Ownership does not change.)
- When the expiration time passes, the user's right to use expires and is reclaimed.
For such services, Ethereum supports several standard contracts. Among them, we will analyze the well-known ERC-4907, Rental NFT contract.
ERC4907 - Rental NFT, an Extension of EIP-721
As the name Rental NFT suggests, ERC-4907 is a representative ERC standard related to NFT rentals. It is an extension of ERC-721 and was proposed by Double Protocol, which provides an NFT Rental protocol, as EIP-4907. It became ERC-4907 when it was confirmed as a standard through community voting. They explained the reason for proposing ERC-4907 as follows.
Some utility NFTs may not always have the same owner and user depending on the situation. In such cases, it is necessary to identify the owner and user separately and manage the authority to perform operations accordingly. Some projects use roles differentiated by names such as 'Operator' and 'Controller', but as this configuration becomes increasingly common, a unified standard is needed to facilitate collaboration between all applications.
Two transactions are needed to grant NFT usage rights. (1. Entering the address for assigning a new user role, 2. Reclaiming the user role) Generating two transactions is inefficient and uneconomical, so we have made it so that the usage period is automatically ended using only one transaction.
They also anticipated the following effects from the introduction of ERC-4907:
💡 Effects of Introduction
Convenient Rights Assignment** - The authority (rights) of the owner and user can be easily managed due to role assignment.
Simple Rental Management - The Expires
function can be used to automatically expire the user's rights, allowing for the reclamation of rights without a separate transaction.
Easy Integration with Third-Party Platforms - Interaction with other projects is possible without the permission of the NFT issuer or application.
Compatibility with Previous Versions - It is fully compatible with ERC721.
Then, let's analyze how the contract code was written to achieve these effects.
IERC4907.sol
- Looking at IERC4907.sol, you can see that there are three functions:
setUser
,userOf
,userExpires
, and there is an event calledUpdateUser
.
interface IERC4907 {
// Logged when the user of an NFT is changed or expires is changed
/// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed
/// The zero address for user indicates that there is no user address
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);
/// @notice set the user and expires of an NFT
/// @dev The zero address indicates there is no user
/// Throws if `tokenId` is not valid NFT
/// @param user The new user of the NFT
/// @param expires UNIX timestamp, The new user could use the NFT before expires
function setUser(uint256 tokenId, address user, uint64 expires) external;
/// @notice Get the user address of an NFT
/// @dev The zero address indicates that there is no user or the user is expired
/// @param tokenId The NFT to get the user address for
/// @return The user address for this NFT
function userOf(uint256 tokenId) external view returns(address);
/// @notice Get the user expires of an NFT
/// @dev The zero value indicates that there is no user
/// @param tokenId The NFT to get the user expires for
/// @return The user expires for this NFT
function userExpires(uint256 tokenId) external view returns(uint256);
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC4907.sol";
contract ERC4907 is ERC721, IERC4907 {
struct UserInfo
{
address user; // address of user role
uint64 expires; // unix timestamp, user expires
}
mapping (uint256 => UserInfo) internal _users;
constructor(string memory name_, string memory symbol_)
ERC721(name_, symbol_)
{
}
/// @notice set the user and expires of an NFT
/// @dev The zero address indicates there is no user
/// Throws if `tokenId` is not valid NFT
/// @param user The new user of the NFT
/// @param expires UNIX timestamp, The new user could use the NFT before expires
function setUser(uint256 tokenId, address user, uint64 expires) public virtual{
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC4907: transfer caller is not owner nor approved");
UserInfo storage info = _users[tokenId];
info.user = user;
info.expires = expires;
emit UpdateUser(tokenId, user, expires);
}
/// @notice Get the user address of an NFT
/// @dev The zero address indicates that there is no user or the user is expired
/// @param tokenId The NFT to get the user address for
/// @return The user address for this NFT
function userOf(uint256 tokenId) public view virtual returns(address){
if( uint256(_users[tokenId].expires) >= block.timestamp){
return _users[tokenId].user;
}
else{
return address(0);
}
}
/// @notice Get the user expires of an NFT
/// @dev The zero value indicates that there is no user
/// @param tokenId The NFT to get the user expires for
/// @return The user expires for this NFT
function userExpires(uint256 tokenId) public view virtual returns(uint256){
return _users[tokenId].expires;
}
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override{
super._beforeTokenTransfer(from, to, tokenId);
if (from != to && _users[tokenId].user != address(0)) {
delete _users[tokenId];
emit UpdateUser(tokenId, address(0), 0);
}
}
}
The analysis of this code is as follows.
Variables
*struct UserInfo**
Declare a structure called UserInfo. UserInfo has an address type user
that stores the borrower's address and a uint64 type expires
that stores the NFT rental expiration period as variables. expires
stores the expiration time as a unixTimestamp.
struct UserInfo
{
address user; // address of user role
uint64 expires; // unix timestamp, user expires
}
mapping (uint256 => UserInfo) internal **_users;**
Declare _users
that maps TokenId and UserInfo. You can access the mapped UserInfo structure using TokenId as the Key value.
TokenId | (user, expires) |
(uint256) | ({UserAddress} , {UnixTimestamp}) |
Method
function setUser(uint256 tokenId, address user, uint64 expires)
It takes the tokenId of the NFT, the address of the user (not the Owner), and the validity period as arguments, checks if msg.sender is the Owner of the NFT, declares a structure named info
with the same structure as UserInfo
, saves the user address and validity period, and then runs the UpdateUser
event to record the data.
function setUser(uint256 tokenId, address user, uint64 expires) public virtual{
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC4907: transfer caller is not owner nor approved");
UserInfo storage info = _users[tokenId];
info.user = user;
info.expires = expires;
emit UpdateUser(tokenId, user, expires);
}
function userOf(uint256 tokenId)
It takes the tokenId of the NFT as an argument and loads the data of the structure mapped to that value. If the expires value stored in the structure is greater than block.timestamp, it returns the Address stored in the mapped structure, and if it is less than block.timestamp, it returns a zeroAddress.
function userExpires(uint256 tokenId) public view virtual returns(uint256){
return _users[tokenId].expires;
}
Event
When the event is executed, the two values uint256 indexed tokenId
and address indexed user
specified as indexed are stored in the Log. The non-indexed uint64 expires
is recorded in the Data Field.
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);
Test & Result
To test ERC-4907, I deployed the contract to the Ethereum Sepolia Testnet and executed the Method.
- Owner:
0x99b1CB2591578A5ceF5F7003CB6c5561B87A3122
- User:
0x0A098Eda01Ce92ff4A4CCb7A4fFFb5A43EBC70DC
- The owner rented the NFT to the user and set
expires
to 5 minutes later. The actual executed transaction can be checked through the link below.
Check Sepolia Testnet Transaction
The owner sets the To address to the contract address and sends the transaction. When the transaction is executed, the UpdateUser Event is executed and the Log is recorded. In the Log, you can see that the TokenId and Address are indexed and recorded, and expires is stored in the Data Field.
The following were returned by executing userOf and userExpires, which can check the address and expiration period of the renter. The ownerOf Method was added to clearly compare the actual owner and the renter. You can see that the renter's address changes to zeroAddress as expires elapses.
Based on these contents, EIP-4907 was selected as ERC-4907 through a vote of the Ethereum community. However, there are things to be aware of before actually using ERC-4907.
setUser
has no exception handling.
setUser
is a function that simply specifies Address and Expires, but because there is no exception handling, you can rent an NFT that is currently being rented to another user without any restrictions. To prevent this, code that checks whether the NFT corresponding to the tokenId entered as a parameter is currently being rented and, if it is being rented, reverts the transaction should be added.
Expires
expiration is a zeroAddress return.
If you look at the userOf function, it is written to return zeroAddress when the Expires
period has expired, without changing the Address and expires stored in the Struct. It is presumed to be for saving gas because a transaction occurs when Address and expires are initialized when expires expires. It is possible to check as zeroAddress for the information of the user whose usage period has expired using the userOf, userExpires Method, but there may be a misunderstanding because the user's Address still remains when querying the data of the Struct.
Usage rights
are not ownership
.
In this code, rental (granting usage rights) is just storing Address and Expires in Struct, not actually transferring NFT. Because the user cannot check the NFT in their wallet, consideration should also be given to implementing a page that can confirm that the user is a user who has rented the NFT.
There are several things to be aware of as above, but it is definitely meaningful that the usability of NFT increases as extended functions of NFT like ERC-4907 appear. It's because it proves the actual need and value of NFT, breaking away from the level that focused only on ownership and inquiry. If you look at the contract that recently became an ERC standard, there is a lot of content related to NFT. Following this trend, watching and experiencing the development of NFT will also be fun.
🔎About Nodit
Nodit is a platform aims to provide reliable node & consistent data infrastructure for scaling your dapps in multi-chain environment. The core technology of Nodit is a data pipeline that performs crawling, indexing, storing, and processing of blockchain data, along with a reliable node operation service. With processed blockchain data, developers can leverage on-chain and off-chain integration, advanced analytics and visualization, and even AI modeling to build exceptional Web3 products.
Homepage l X (Twitter) l Linkedin
Join us and build more👊🏻
👉Start for Free (Click)
📩 Email: nodit@lambda256.io