Alert Source Discuss
Standards Track: ERC

ERC-4907: Rental NFT, EIP-721 的扩展

向 EIP-721 代币添加具有受限权限的、有时间限制的角色。

Authors Anders (@0xanders), Lance (@LanceSnow), Shrug <shrug@emojidao.org>
Created 2022-03-11
Requires EIP-165, EIP-721

摘要

本标准是 EIP-721 的扩展。它提出了一个额外的角色 (user),可以授予给地址,以及一个角色自动撤销的时间 (expires)。user 角色代表“使用” NFT 的权限,但不能转移它或设置用户。

动机

一些 NFT 具有某些效用。例如,虚拟土地可以被“使用”来构建场景,而代表游戏资产的 NFT 可以在游戏中被“使用”。在某些情况下,所有者和用户可能并不总是同一个人。可能存在 NFT 的所有者将其出租给“用户”。“用户”应该能够对 NFT 执行的操作与“所有者”不同(例如,“用户”通常不应该能够出售 NFT 的所有权)。在这些情况下,拥有单独的角色来识别地址是代表“所有者”还是“用户”,并相应地管理执行操作的权限是有意义的。

一些项目已经以不同的名称(例如“operator”或“controller”)使用这种设计方案,但随着它变得越来越普遍,我们需要一个统一的标准来促进所有应用程序之间的协作。

此外,这种模型的应用(如租赁)通常要求用户地址只能临时访问使用 NFT。通常,这意味着所有者需要提交两个链上交易,一个交易在持续时间开始时将新地址列为新的用户角色,另一个交易在结束时回收用户角色。这在劳动力和 gas 方面都是低效的,因此引入了一个“expires”函数,该函数将有助于自动结束使用期限,而无需第二次交易。

规范

本文档中的关键词“必须”,“禁止”,“需要”,“应该”,“不应该”,“推荐”,“可以”和“可选”应按照 RFC 2119 中的描述进行解释。

合约接口

带有 NatSpec 和 OpenZeppelin v4 接口的 Solidity 接口(也可在 IERC4907.sol 中找到):

interface IERC4907 {

    // Logged when the user of an NFT is changed or expires is changed
    /// @notice 当 NFT 的 `user` 更改或 `user` 的 `expires` 更改时发出
    /// 当 user 的地址为零地址时,表示没有 user 地址
    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    /// @notice 设置 NFT 的 user 和 expires
    /// @dev 零地址表示没有 user
    /// 如果 `tokenId` 不是有效的 NFT 则抛出
    /// @param user  NFT 的新 user
    /// @param expires  UNIX 时间戳,新的 user 可以在 expires 之前使用 NFT
    function setUser(uint256 tokenId, address user, uint64 expires) external;

    /// @notice 获取 NFT 的 user 地址
    /// @dev 零地址表示没有 user 或 user 已过期
    /// @param tokenId 要获取 user 地址的 NFT
    /// @return 此 NFT 的 user 地址
    function userOf(uint256 tokenId) external view returns(address);

    /// @notice 获取 NFT 的 user 过期时间
    /// @dev 零值表示没有 user
    /// @param tokenId 要获取 user 过期时间的 NFT
    /// @return 此 NFT 的 user 过期时间
    function userExpires(uint256 tokenId) external view returns(uint256);
}

userOf(uint256 tokenId) 函数可以实现为 pureview

userExpires(uint256 tokenId) 函数可以实现为 pureview

setUser(uint256 tokenId, address user, uint64 expires) 函数可以实现为 publicexternal

当用户地址更改或用户过期时间更改时,必须发出 UpdateUser 事件。

当使用 0xad092b5c 调用 supportsInterface 方法时,必须返回 true

理由

此模型旨在方便实施。以下是本标准解决的一些问题:

清晰的权利分配

通过双重“owner”和“user”角色,管理贷方和借方可以和不能对 NFT 做什么(换句话说,他们的权利)变得更加容易。 此外,所有者可以控制谁是用户,并且其他项目可以轻松地将自己的权利分配给所有者或用户。

简单的链上时间管理

一旦租赁期结束,就需要重置用户角色,并且“user”必须失去使用 NFT 的权利。这通常通过第二个链上交易完成,但这效率低下并且可能导致复杂化,因为它不精确。使用 expires 函数,无需进行另一次交易,因为“user”会在持续时间结束后自动失效。

轻松的第三方集成

本着无需许可的互操作性的精神,此标准使第三方协议可以更轻松地管理 NFT 使用权,而无需 NFT 发行者或 NFT 应用程序的许可。一旦一个项目采用了额外的 user 角色和 expires,任何其他项目都可以直接与这些功能进行交互并实施他们自己的交易类型。例如,使用此标准的 PFP NFT 可以集成到租赁平台(用户可以在其中租赁 NFT 30 天)并且同时集成到抵押贷款平台(用户可以使用 NFT,同时最终通过分期付款购买 NFT 的所有权)。所有这些都无需获得原始 PFP 项目的许可即可完成。

向后兼容性

如规范部分所述,通过添加扩展函数集,此标准可以完全与 EIP-721 兼容。

此外,本标准中引入的新函数与 EIP-721 中的现有函数有许多相似之处。这使开发人员可以快速轻松地采用该标准。

测试用例

测试合约

ERC4907Demo 实现:ERC4907Demo.sol

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

import "./ERC4907.sol";

contract ERC4907Demo is ERC4907 {

    constructor(string memory name, string memory symbol)
     ERC4907(name,symbol)
     {         
     }

    function mint(uint256 tokenId, address to) public {
        _mint(to, tokenId);
    }

}

测试代码

test.js

const { assert } = require("chai");

const ERC4907Demo = artifacts.require("ERC4907Demo");

contract("test", async accounts => {

    it("should set user to Bob", async () => {
        // Get initial balances of first and second account.
        // 获取第一个和第二个帐户的初始余额。
        const Alice = accounts[0];
        const Bob = accounts[1];

        const instance = await ERC4907Demo.deployed("T", "T");
        const demo = instance;

        await demo.mint(1, Alice);
        let expires = Math.floor(new Date().getTime()/1000) + 1000;
        await demo.setUser(1, Bob, BigInt(expires));

        let user_1 = await demo.userOf(1);

        assert.equal(
            user_1,
            Bob,
            "User of NFT 1 should be Bob"
        );

        let owner_1 = await demo.ownerOf(1);
        assert.equal(
            owner_1,
            Alice ,
            "Owner of NFT 1 should be Alice"
        );
    });
});


在终端中运行:

truffle test ./test/test.js

参考实现

实现:ERC4907.sol

// 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);
        }
    }
} 

安全考虑

此 EIP 标准可以完全保护所有者的权利,所有者可以随时更改 NFT 用户和过期时间。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Anders (@0xanders), Lance (@LanceSnow), Shrug <shrug@emojidao.org>, "ERC-4907: Rental NFT, EIP-721 的扩展," Ethereum Improvement Proposals, no. 4907, March 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4907.