一 为什么需要ERC-4907
当有时候,NFT价格太贵或者没有对应的NFT所有权,但是又想使用NFT,用来从事一些商业活动。这时候,ERC-721是没办法来解决这个问题,对于ERC-721来说,NFT只能发生所有权转移,不能再所有权未转移的前提下使用别人的NFT。
因此,为了适配这种场景,提出来了ERC-4907提案,该提案允许用户从拥有NFT所有权的用户中租赁NFT一定时间的使用权
二 ERC-4907标准提案
2.1 什么是ERC-4907提案
第一: ERC-4907是一个以太坊NFT 租赁协议标准提案
第二: 它提出了所有权和使用权的概念,并且所有权和使用权是分离的
第三: 它允许用户将NFT的使用权租赁给别人,并且可以设置租赁到期时间,到期后租赁自动归还给NFT的所有者
2.2 ERC-4907提案具有哪些特点
2.2.1 灵活的租赁机制
ERC-4907允许NFT所有者在不转让完全所有权的情况下,租赁其NFT给他人。这种灵活性使得NFT可以在不同的时间段内被多个用户使用,从而提升了其经济效益和使用率
2.2.2 增加了NFT的流动性
通过租赁模型,ERC-4907使得NFT更易于流动。持有者可以选择将其资产租给需要的用户,而非必须完全出售,从而拓展了NFT在市场上的应用方式
2.2.3 租赁流程简单
ERC-4907定义了明确的用户角色和使用权期限,通过智能合约自动管理使用权的到期和更新,从而简化了租赁过程,减少了操作成本和风险。
2.2.4 促进了新的商业模式
该标准为NFT租赁和订阅模型开辟了新的商业机会,例如游戏内物品租赁、虚拟资产租赁等,增加了数字资产的多样化利用方式。
2.3 ERC-4907提案具有哪些使用场景
2.3.1 资产抵押和借贷
NFT所有者可以使用ERC-4907将其资产租赁给他人,并将租金作为抵押品用于借贷或其他金融交易
2.3.2 订阅和服务模型
ERC-4907允许创建基于订阅的NFT服务模型,例如内容订阅或专业服务订阅。用户可以支付租金以获取NFT的临时使用权,这种模式适用于数字内容市场、教育资源或其他服务领域
2.3.3 艺术品和数字创作的临时使用权
艺术家或数字创作者可以使用ERC-4907来提供其作品的临时使用权,例如数字艺术品或音乐作品。这使得艺术品可以在限定时间内展示或使用,而所有者仍然保留作品的完全控制权
2.3.4 虚拟地产租赁
在虚拟世界或元宇宙中,用户可以通过ERC-4907标准租赁虚拟地产或其他数字资产。这种模式允许用户在特定时间内使用虚拟空间,进行展示、活动或其他商业用途,而不需要长期的资产拥有权。
2.3.5 游戏内物品租赁
游戏开发者可以使用ERC-4907标准实现在游戏中租赁虚拟物品,如武器、服装或其他游戏资产。玩家可以通过租赁获取对这些稀有或高价值物品的临时使用权,而不必购买它们的完整所有权
三 ERC-4970标准提案规范
3.1 函数
3.1.1 setUser
function setUser(uint256 tokenId, address user, uint64 expires) external;
设置哪一个用户享有使用权,到期时间是多长
3.1.2 userOf
function userOf(uint256 tokenId) external view returns (address);
根据tokenId获取当前哪一个用户持有对该NFT的使用权
3.1.3 userExpires
function userExpires(uint256 tokenId) external view returns (uint256);
获取一个tokenId对应的NFT的如果有人持有使用权,那么到期时间是多少?
3.2 事件
3.2.1 UpdateUser
event UpdateUser(
uint256 indexed tokenId,
address indexed user,
uint64 expires
);
四 手动实现ERC-4907
4.1 IERC4907.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface IERC4907 {
// Emitted when the `user` of an NFT or the `expires` of the `user` is changed
event UpdateUser(
uint256 indexed tokenId,
address indexed user,
uint64 expires
);
/**
* Set the user and expires of a NFT
* @param tokenId token id
* @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;
/**
* Get the user address of an NFT
* @param tokenId The NFT to get the user address for
*/
function userOf(uint256 tokenId) external view returns (address);
/**
* Get the user expires of an NFT
* @param tokenId The NFT to get the user expires for
*/
function userExpires(uint256 tokenId) external view returns (uint256);
}
4.2 ERC4907.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IERC4907.sol";
import "./IERC4907Errors.sol";
import "./Counter.sol";
abstract contract ERC4907 is IERC4907, ERC721 {
struct UserInfo {
address user; // address of user role
uint64 expires; // unix timestamp, user expires
}
mapping(uint256 tokenId => UserInfo) private _users;
Counter private _counter;
event NFTMinted(address indexed user, uint256 indexed tokenId);
constructor(string memory name, string memory symbol) ERC721(name,symbol) {
}
function setUser(
uint256 tokenId,
address user,
uint64 expires
) external override {
// check caller if has permission to set user, only owner or authorized spender could do this
require(_isApprovedOrOwner(msg.sender, tokenId), "transfer caller is not owner or not approved");
// check token if has rent to user, if not, the user address should be zero address
require(userOf(tokenId) == address(0), "User already assigned");
// check expires if greater than the current block timestamp
require(expires > block.timestamp, "expires should be in future");
ExpiresLessThanCurrentTime
UserInfo storage info = _users[tokenId];
info.user = user;
info.expires = expires;
emit UpdateUser(tokenId, user, expires);
}
/**
* Check the assigned user of token, if no any assigned or expired, return address(0)
* @param tokenId token id
*/
function userOf(uint256 tokenId) public view override returns (address) {
if (_users[tokenId].expires <= block.timestamp) {
return address(0);
}
return _users[tokenId].user;
}
function userExpires(uint256 tokenId) external view override returns (uint256) {
return _users[tokenId].expires;
}
function transferFrom(address from, address to, uint256 tokenId) public virtual {
_beforeTokenTransfer(from, to, tokenId);
super.transferFrom(from, to, tokenId);
}
function mint() public returns (uint256) {
_counter.increment();
uint256 tokenId = _counter.current();
_safeMint(msg.sender, tokenId);
emit NFTMinted(msg.sender, tokenId);
return tokenId;
}
function _isApprovedOrOwner(
address sender,
uint256 tokenId
) internal view virtual returns (bool) {
address owner = _ownerOf(tokenId);
address spender = _getApproved(tokenId);
return (sender == owner || sender == spender);
}
function supportsInterface(bytes4 interfaceId ) public view virtual override(ERC721) returns (bool) {
return interfaceId == type(IERC4907).interfaceId || super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override {
if (
from != to &&
_users[tokenId].user != address(0) && //user present
block.timestamp >= _users[tokenId].expires //user expired
) {
delete _users[tokenId];
emit UpdateUser(tokenId, address(0), 0);
}
}
}
4.3 Counter.sol
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
function current() public returns (uint256){
return number;
}
}