EIP4907: ERC-721 User And Expires Extension

It’s a shame that the authors of this standard didn’t wait a bit longer before flagging it as final as I’m noticing slight inconsistencies with the surrounding ecosystem.

E.g. EIP-721 which this contract aspires to be compatible with has a similar function to function userOf(uint256 tokenId) but in case the tokenId doesn’t exist, it throws. See OZ’s implementation. So it is inconsistent.

EIP-4907, however, made the odd choice of returning the zero address as a signal, which I judge as a bad decision given that I conceptually see function ownerOf to be very similar to function userOf.

And before someone misunderstands: 4907 mandates returning the zero address in the standard and 721 mandates throwing so these are NOT implementation details, they are interface inconsistencies!

eip-721.md

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);

eip-4907.md

    /// @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);
2 Likes

Cleaned up trailing white space, formatting and repackaged ERC4907 as a foundry package here: GitHub - rugpullindex/ERC4907: Reference Implementation of EIP-4907 "Rental NFT, ERC-721 User And Expires Extension"

1 Like

Thanks for your feedback. Because the status of ERC-4907 is Final, you could propose a new EIP as extension of ERC-4907 to add this new function.

Hi, thanks for your work on this EIP to allow the rental of NFT while keeping ownership flexibility

This is something I have been working on in the past and I actually got my own proposal which in my opinion offers significant benefits over the one proposed here. I already have a repo exploring the idea in great detail, see here: GitHub - wighawag/erc721-lease: A contract to manage lease of NFT

I do not want to hijack the discussion here and so I also created a topic to discuss the proposal on its own here: ERC721 Lease: allowing owner to rent NFT to other

To recapitulate, the main difference with EIP-4907 is that my proposal

  • work with all ERC721, past, present and future
  • does not need to get implemented in the token contract
  • is completely generic: can implement any kind of contract between the user and owner.
  • 's rental representation is itself an ERC721, so no extra work to get it working with existing infrastructure or tooling
  • puts users (lease owners) and NFT owners on the same footing. EIP-4907 on the other end does not protect the “users” in any way as the owner can reclaim at any time and thus does not establish a fair ground for owner/user agreement

The main pain point I see for EIP-4907 is that “usership” expiry is not enforced in any way and NFT owner has full power. This takes a political stance on the matter of ownership vs rental rights while such a system should be as fair as possible to allow user and owner to set their own term.

3 Likes

From a business and legal point of view, handling the ownership of any asset is a problem solved by ancient romans lawyers. Basically, you need to handle 3 properties:

  • Who is the owner of an assets, basically who is able to transfer the ownership of the asset, and by default setting is also the owner of any other rigths.
  • Who can use the asset. For instance, the one who lives on a rented house
  • Who can take the benefits from the asset (romans call this ius fruendi). Using the same rented house example, the landlord of the house is the one who owns this right.
    This way of modeling this has worked over 2.000 years… honestly I doubt any one in the crypto world can make something better.
2 Likes

I will be happy to chat more. Please check this out: → EIP-tbd Rental & Delegation NFT - ERC-721 Extension

Great, i would like to chat more. :slight_smile: Please, check this out: → EIP-tbd Rental & Delegation NFT - ERC-721 Extension

Great point, please check this out: → EIP-tbd Rental & Delegation NFT - ERC-721 Extension And let us know, what you think. :slight_smile:

1 Like
1 Like

What is the consensus on this EIP? What projects are using it?

This seems like a pretty serious issue that I have not seen addressed:

1 Like

When I was testing ERC-4907 contract, I found out something. Let’s say an NFT with token id:1 was minted on user(1) and rented to user(2) for 3 days. Now during these 3 days, whenever user(1) transfers that NFT to let’s assume user(3), user(2) which should have access to rented NFT for 3 days, now no longer have access to that NFT. userOf(1) sets to address(0). This can be overcome by adding modifiers and overriding transfer and safeTransfer functions of ERC-721.

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "./IERC4907.sol";

contract ERC4907 is ERC721, IERC4907 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdCounter;


    struct UserInfo
    {
        address user;   // address of user role
        uint64 expires; // unix timestamp, user expires
    }

    mapping (uint256  => UserInfo) internal _users;

    modifier notRented(uint256 tokenId){
        require(userOf(tokenId)==address(0),"can not transfer rented NFT");
        _;
    }

    constructor(string memory name_, string memory symbol_)
     ERC721(name_,symbol_)
     {
     }



    function safeMint(address to) public {
        _tokenIdCounter.increment();
        uint256 tokenId = _tokenIdCounter.current();
        _safeMint(to, tokenId);
    }

    /// @notice set the user and expires of a 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),"ERC721: transfer caller is not owner nor approved");
        UserInfo storage info =  _users[tokenId];
        info.user = user;
        info.expires = expires;
        emit UpdateUser(tokenId,user,expires);
    }
    function getTimestamp()public view returns(uint256){
        return block.timestamp +300;
    }

    /// @notice Get the user1664725514 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);
        }
    }

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override notRented(tokenId){
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");

        _transfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override notRented(tokenId){
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev See {IERC721-safeTransferFrom}.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) public virtual override notRented(tokenId){
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
        _safeTransfer(from, to, tokenId, data);
    }

}
1 Like

Hey mate, great documentation. Are these features approved and ready to be implemented into any dApp ? or do you need to get it approved first.

thank you

For rental protocol, to protect the use rights, the ERC-4907 NFT can be staked to a contract.

Double Protocol Lending Example:
Glossary:

  • oNFT: original NFT, which is an ERC-4907 NFT
  • doNFT: DOUBLE NFT. doNFT complies with the ERC-721 standard and represents a certificate for the right to use the NFT within a specific time.
  • vNFT: A special doNFT, as a voucher to redeem the original NFT.

Steps:
1 Alice calls setApprovalForAll() function of oNFT contract, approves doNFT contract can transfer the oNFT token.

2 Alice calls mintVNft() function of the doNFT contract to mint a vNFT token:

2.1 doNFT contract calls transferFrom() function of oNFT contract.

2.2 doNFT contract transfers the oNFT token to the doNFT contract. After that, the owner of the oNFT will become the doNFT contract.

2.3 doNFT contract mints a vNFT token and transfers it to Alice.

2.4 doNFT contract calls setUser() function of oNFT contract.

2.5 Set user to Alice so that Alice can use the oNFT when the oNFT is not rented.

3 Alice calls createLendOrder() function of the market contract, sets the rental price, period, and creates a lending order in the market.

More details: ERC-4907 Model Timing Diagram - Double

Some NFTs can be rented on double.one:

Some ERC-4907 contracts:

symbol chain contract address
POP Ethereum 0xe1fb64bcefd7392aa0ec39354618ac48a5ddc501
INF Ethereum 0x8e5324d34ee9ab2ed84ac9ba237ca0433e89130c
OSF Ethereum 0x715cbae9d7fcf899e69221368be587f6cb39e4c6
GAVA Ethereum 0x4302b24ea1010b4cb4f1f056341334ccd2efd3c1
AFP Ethereum 0xa13b8cbd068fe176b3d37c6694173064e17ef563
MDT Ethereum 0x86eea52fd8ed3ee84bc135b284a589e4ed4ebf14
ADCCC Ethereum 0x4711e4ea574c72501be4922224cfa38bc281d16d
eyecon Ethereum 0x8d95a85f5f6dafeffad29dfe08869c1f9dd00802
METAHOME Ethereum 0x3872beeb0edef42f39e3b8e7d5339352f22d319a
XSOLDIERS Ethereum 0x0e559f7771d8fbd2dda30a4eae12c07179907d53
NTR Ethereum 0xbf8f831c9b90038fdfbf034740f624fdc63f3217
MD Ethereum 0xbedee3879a3c5107d8846bf7d91dbaacf8d42544
SHARKZG Ethereum 0x91220b88311dcd521e0d68466ef39514a1baeebc
cocoisland Ethereum 0xd36c2731078d8490bae33ba5e05bf8b99abdc913
VXIN Ethereum 0x2f31f763911a79fccefd3014b1fdb9dd8dbadf7f
wetw Ethereum 0xd446e68714b61acdb2c51076fe16f127b94c9328
LSC Ethereum 0x491f8d040e90a8a9681f6afe9dfb114f2dbb960c
MCAstro Ethereum 0x9df90628d40c72f85137e8cee09dde353a651266
SUSUALPHA Ethereum 0x29e0a58f62a34a29965ac5c64f5f3c792bee7a9a
HMLAP Ethereum 0x0d618537548d769bd30f5c8e19f2b8beb7d83f06
CAHMAP Ethereum 0x512926bc2266db6546f3d3c11a383b3d0bf19a11
ZAHMAP Ethereum 0x44da50f9ce1557cb8b9e04bda0fb9e84172a44b7
T2WEB Ethereum 0x75d0bf98e2dbf8248c2545e4fbbb1c0247ef7508
wWarena BSC 0x6793619c3dd9d545520923da47c6f8824e2cc30f
SKNFT BSC 0x9835b7731017f52adadbb1d9f2e3aaa88c7d5db6
EGNFT BSC 0xe5fea8173f817a758f265c5d8f23790df8a311e0
MECH_HERO BSC 0x959924fcfb62cbd1b2e4ed50fadd1b469f0496b8
BSONE BSC 0x01fccf2b10c0b3d579e98574ef0488f1b05a3824
BBBONE BSC 0xadf74b09c6fead7cbac1c21eb3a583f4b2e10cf1
EGNFT BSC 0x22eb0f44406e7d0631600c5efa3158f648825e95
T2WEB BSC 0x94a3695e8a2e7dca189c3a1ace3b767492fa214d
POSTCARD BSC 0xcfa332e7a7994d41cc52a72bbabd7f04c4f15bb0
wNCSHIP Polygon 0x126519a9070fe4b4e005c86a55edaa0750801fa9
wNC Polygon 0xc30dedd81fe3cd756bffee41199e86b0c3b10218
wZUKINFT Polygon 0x945af2f45290c2a8ef4f651f35ae2413bee8e4b7
E4T Polygon 0x374155a0c6ed23046f4f17a0b58995cace6f5bce
T2WEB Polygon 0x75d0bf98e2dbf8248c2545e4fbbb1c0247ef7508
LCC Polygon 0x491f8d040e90a8a9681f6afe9dfb114f2dbb960c
PDLS Polygon 0xb89d3d7ca23a34ca75f648072ec762c857ee0efd
LGC Polygon 0x84c19001c1c75ef1a869023843d4865f231a3ede
newNft Polygon 0x24fd77efa4e0a7629154dec2d18a82af2ba5126e
LPCP Polygon 0x57dc9bb707c687c056e510a0e7b3a8b98e7bec2a

Hello, i’m new here on ethmagicians
I’m actually wondering if the final stage means “this implementation = standard” or if the just the interface should be considered as such, thus not setting in the stone the implemention given in gitHub
Not sure if I should discuss in a different EIP or the discusssion on the implementation goes on even when the EIP is marked as “final”.

Anyway i’ll make my question here

Wouldn’t an implementation like this just fix the usership problem while enabling non-custodial protocol to handle 4907 or you can see major iussues doing like:

  1. When the usership expires returns the ownerOf() instead of address 0.
  function userOf(uint256 tokenId) public view virtual override returns(address){
        if( userExpires(tokenId) >=  block.timestamp){
             return  _users[tokenId].user;
         }
         else{
             return address(ownerOf(tokenId));
         }
    }
  1. Prevent usership to be set again before the current expires.

By allowing setUser to be changed while user is not expired, it becomes mandatory to have an intermediary contract to own the NFT, to prevent the real owner to call setUser() or transfer triggering the delete info[tokenId]
This makes possible for a renter to delegate a contract to setUser instead, thus not requiring custody. In this scenario the user is guaranteed to retain usership from start to end.

function setUser(uint256 tokenId, address user, uint256 expires) public virtual override {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC4907: transfer caller is not owner nor approved");
        require(userOf(tokenId) != ownerOf(tokenId), "ERCXXX: user can't be set before expiration");   
        UserInfo storage info =  _users[tokenId];
        info.user = user;
        info.expires = expires;
        emit UpdateUser(tokenId, user, expires);
    }
  1. Don’t override _beforeTokenTransferanymore

At this point if the user paid his interest for a rental, transfer shoudn’t delete info at all and let third party markets to mark the item as rented using IERC4907.
It will then be on buyer to evaluate wheter or not the deal is convienent or not to him. Think it as a bare ownership more or less

Even tho I’m not sure if this discussion is over or not, i would like to contribute with my opinion. Nice work so far

2 Likes

Wait, whether it sounds not-fair? I am the owner of this NFT(Azuki), and I rental the ownership to “many” people.

nice nice feedback!!!

The status field of an EIP only describes the document itself and says nothing about the ecosystem or whether people are actually using the EIP. In short, Final here means: EIP-4907 will not change besides errata and text formatting.

1 Like

The implementation of EIP-4907 is a reference implementation, you can do a different implementation.

We removed _beforeTokenTransfer in our newest implementation:

override _burn function

    function _burn(uint256 tokenId) internal virtual override {
        super._burn(tokenId);
        delete _users[tokenId];
        emit UpdateUser(tokenId, address(0), 0);
    }