Rentero Protocol

Rentero Protocol is compatible with EIP-4907 in the separation of NFT usage-right and ownership, and extends EIP-4907. In the implementation of EIP-4907, NFT owners can revoke users’ right to use NFTs at any time; hence, users’ rights cannot be protected well. Rentero Protocol adds a new role, i.e., delegator. Under Rentero Protocol, an NFT owner licenses the power to allocate the usage-right to a delegator, who has the right to set the NFT user and expires, a user can acquire the right to use the NFT from the delegator, and the owner has no right to directly revoke the usage-right. A delegator exercises its power through smart contracts, and the contract logic can protect the rights of both NFT owners and users. For example, in the scenario of installment payment, a user who fails to pay the next installment of rent will be deprived of the right to use NFTs by the delegator, thus safeguarding the owner’s income right. For another example, if an NFT owner desires to adjust the rent and change the user during the rental period, it should pay liquidated damages to the current user through the delegator, after which the delegator will reset the new user and expires. Each ERC721 contract corresponds to a Rentero Protocol contract, and Rentero Protocol is also the implementation of ERC721 contracts. Token is called ReNFT. When an owner pledges the native NFT under Rentero Protocol, Rentero Protocol will mint a ReNFT with the same tokenId and sent it to the owner’s address. With ReNFT, the owner will have the ownership of the native NFT with the corresponding tokenId, and be able to redeem the native NFT under Rentero Protocol and transfer the ownership of the native NFT by transferring ReNFT. After the native NFT is redeemed, the ReNFT with the corresponding tokenId will be destroyed, and the delegator will lose the right to allocate NFT usage-right. Delegator contracts can be implemented in different business models, e.g., installment payment, free trial and commission sharing. NFT owners can freely choose their delegators according to their own needs, but each owner can only have one delegator at a time.

Rentero Protocol Interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IRentero is IERC165 {
    event Stake(uint256 indexed tokenId, address indexed owner);

    event Redeem(uint256 indexed tokenId, address indexed owner);

    event UpdateDelegator(uint256 indexed tokenId, address indexed delegator);

    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    function nftAddress() external view returns (address);

    function setDelegator(uint256 tokenId, address delegator) external;

    function stake(uint256 tokenId, address owner) external;

    function redeem(uint256 tokenId) external;

    function setUser(uint256 tokenId, address user, uint64 expires) external;

    function ownerOf(uint256 tokenId) external view returns (address);

    function delegatorOf(uint256 tokenId) external view returns (address);

    function userOf(uint256 tokenId) external view returns (address);

    function userExpires(uint256 tokenId) external view returns (uint256);

    function canUseToken(address operator, uint256 tokenId) external view returns (bool);

    function canUseTokens(address operator) external view returns (uint256[] memory);
}so

Rentero Protocol Implementation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "./IRentero.sol";

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";

contract RenteroERC721 is IRentero, ERC721, ERC721Holder {
    using EnumerableSet for EnumerableSet.UintSet;
    // nft contract address
    address public immutable override nftAddress;

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

    mapping(uint256 => UserInfo) internal _tokenIdToUserInfo;
    mapping(uint256 => address) internal _tokenIdToDelegator;
    mapping(address => EnumerableSet.UintSet) internal _userCanUseTokens;

    constructor(
        address nftAddress_,
        string memory name_,
        string memory symbol_
    ) ERC721(name_, symbol_) {
        bytes4 interfaceId = type(IERC721).interfaceId;
        require(IERC721(nftAddress_).supportsInterface(interfaceId), "Not support contract.");
        nftAddress = nftAddress_;
    }

    function setDelegator(uint256 tokenId, address delegator) external virtual override {
        require(_onlyApprovedOrOwner(msg.sender, tokenId), "Not approved or owner.");

        _tokenIdToDelegator[tokenId] = delegator;

        emit UpdateDelegator(tokenId, delegator);
    }

    // user should call nft contract approve(delegator,tokenId) before stake
    function stake(uint256 tokenId, address owner) external virtual override {
        address delegator = _tokenIdToDelegator[tokenId];
        require(msg.sender == delegator, "Not delegator.");

        require(IERC721(nftAddress).ownerOf(tokenId) == address(this), "No stake.");

        ERC721._safeMint(owner, tokenId);

        emit Stake(tokenId, owner);
    }

    function redeem(uint256 tokenId) external virtual override {
        address delegator = _tokenIdToDelegator[tokenId];
        require(msg.sender == delegator, "Not delegator.");

        address owner = ERC721.ownerOf(tokenId);

        ERC721._burn(tokenId);

        UserInfo memory info = _tokenIdToUserInfo[tokenId];
        _userCanUseTokens[info.user].remove(tokenId);

        delete _tokenIdToUserInfo[tokenId];

        delete _tokenIdToDelegator[tokenId];

        IERC721(nftAddress).safeTransferFrom(address(this), owner, tokenId);

        emit Redeem(tokenId, owner);
    }

    function setUser(
        uint256 tokenId,
        address user,
        uint64 expires
    ) external virtual override {
        // only delegator can set user
        address delegator = _tokenIdToDelegator[tokenId];
        require(msg.sender == delegator, "Not delegator.");

        require(IERC721(nftAddress).ownerOf(tokenId) == address(this), "No stake.");

        UserInfo storage info = _tokenIdToUserInfo[tokenId];

        _userCanUseTokens[info.user].remove(tokenId);
        _userCanUseTokens[user].add(tokenId);

        info.user = user;
        info.expires = expires;

        emit UpdateUser(tokenId, user, expires);
    }

    function ownerOf(uint256 tokenId) public view virtual override(ERC721, IRentero) returns (address) {
        return ERC721.ownerOf(tokenId);
    }

    function delegatorOf(uint256 tokenId) external view virtual override returns (address) {
        return _tokenIdToDelegator[tokenId];
    }

    function userOf(uint256 tokenId) public view virtual override returns (address) {
        UserInfo memory info = _tokenIdToUserInfo[tokenId];
        if (uint256(info.expires) >= block.timestamp) {
            return info.user;
        } else {
            return address(0);
        }
    }

    function userExpires(uint256 tokenId) external view override returns (uint256) {
        UserInfo memory info = _tokenIdToUserInfo[tokenId];
        if (uint256(info.expires) >= block.timestamp) {
            return uint256(info.expires);
        } else {
            return 0;
        }
    }

    function canUseToken(address operator, uint256 tokenId) external view virtual override returns (bool) {
        // token in use
        UserInfo memory info = _tokenIdToUserInfo[tokenId];
        if (info.user == operator && uint256(info.expires) >= block.timestamp) {
            return true;
        }
        // token not in use
        if (userOf(tokenId) == address(0) && ERC721.ownerOf(tokenId) == operator) {
            return true;
        }
        return false;
    }

    function canUseTokens(address operator) external view virtual override returns (uint256[] memory) {
        return _userCanUseTokens[operator].values();
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
        return interfaceId == type(IRentero).interfaceId || super.supportsInterface(interfaceId);
    }

    function _onlyApprovedOrOwner(address operator, uint256 tokenId) internal view returns (bool) {
        address owner = IERC721(nftAddress).ownerOf(tokenId);
        return (operator == owner ||
            ERC721(nftAddress).getApproved(tokenId) == operator ||
            ERC721(nftAddress).isApprovedForAll(owner, operator));
    }
}

It is worth noting that when there is no user to use an NFT pledged under Rentero Protocol, the NFT owner (i.e., ReNFT holder) can call canUseToken (address operator, uint256 tokenId) as the operator, which returns true, that is, an NFT owner can use an NFT when there is no user. This can reduce the unnecessary redemption operations of NFT owners in practice (incorporated into the code comments)

Last updated