Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7507: 多用户 NFT 扩展

ERC-721 的一个扩展,允许一个 token 拥有多个具有受限权限的用户。

Authors Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang)
Created 2023-08-24
Discussion Link https://ethereum-magicians.org/t/eip-7507-multi-user-nft-extension/15660
Requires EIP-721

摘要

本标准是 ERC-721 的扩展。它为 token 提出了一个新的角色 user,作为 owner 的补充。一个 token 可以有多个用户,每个用户都有单独的到期时间。它允许订阅模式,在这种模式下,不同的用户可以非独占地订阅一个 NFT。

动机

一些 NFT 代表 IP 资产,并且 IP 资产需要被授权访问,而无需转移所有权。订阅模式是 IP 授权的一种非常常见的做法,多个用户可以订阅一个 NFT 以获得访问权限。每个订阅通常都有时间限制,因此会记录到期时间。

现有的 ERC-4907 引入了类似的功能,但不允许超过一个用户。它更适合在租赁场景中使用,在这种场景中,一个用户在下一个用户之前获得对 NFT 的独家使用权。这种租赁模式在代表游戏中的物理资产的 NFT 中很常见,但对于可共享的 IP 资产来说并不是很有用。

规范

Solidity 接口可在 IERC7507.sol 中找到:

interface IERC7507 {

    /// @notice 当 NFT 的用户的到期时间发生变化时发出
    event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);

    /// @notice 获取 NFT 的用户到期时间
    /// @param tokenId 要获取用户到期时间的 NFT
    /// @param user 要获取到期时间的用户
    /// @return 此 NFT 的用户到期时间
    function userExpires(uint256 tokenId, address user) external view returns(uint256);

    /// @notice 设置 NFT 的用户到期时间
    /// @param tokenId 要设置用户到期时间的 NFT
    /// @param user 要设置到期时间的用户
    /// @param expires 用户可以在 UNIX 时间戳的 expires 之前使用 NFT
    function setUser(uint256 tokenId, address user, uint64 expires) external;

}

原理

该标准补充了 ERC-4907 以支持多用户功能。因此,建议的接口尝试保持一致,对函数和参数使用相同的命名。

但是,我们没有包含相应的 usersOf(uint256 tokenId) 函数,因为这意味着实现必须支持枚举多个用户。这并非总是必要的,例如,在开放订阅的情况下。因此,我们决定不将其添加到接口中,而将选择权留给实现者。

向后兼容性

未发现向后兼容性问题。

测试用例

测试用例可在:ERC7507.test.ts 中找到:

import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

const NAME = "NAME";
const SYMBOL = "SYMBOL";
const TOKEN_ID = 1234;
const EXPIRATION = 2000000000;
const YEAR = 31536000;

describe("ERC7507", function () {

  async function deployContractFixture() {
    const [deployer, owner, user1, user2] = await ethers.getSigners();

    const contract = await ethers.deployContract("ERC7507", [NAME, SYMBOL], deployer);
    await contract.mint(owner, TOKEN_ID);

    return { contract, owner, user1, user2 };
  }

  describe("Functions", function () {
    it("Should not set user if not owner or approved", async function () {
      const { contract, user1 } = await loadFixture(deployContractFixture);

      await expect(contract.setUser(TOKEN_ID, user1, EXPIRATION))
        .to.be.revertedWith("ERC7507: caller is not owner or approved");
    });

    it("Should return zero expiration for nonexistent user", async function () {
      const { contract, user1 } = await loadFixture(deployContractFixture);

      expect(await contract.userExpires(TOKEN_ID, user1)).to.equal(0);
    });

    it("Should set users and then update", async function () {
      const { contract, owner, user1, user2 } = await loadFixture(deployContractFixture);

      await contract.connect(owner).setUser(TOKEN_ID, user1, EXPIRATION);
      await contract.connect(owner).setUser(TOKEN_ID, user2, EXPIRATION);

      expect(await contract.userExpires(TOKEN_ID, user1)).to.equal(EXPIRATION);
      expect(await contract.userExpires(TOKEN_ID, user2)).to.equal(EXPIRATION);

      await contract.connect(owner).setUser(TOKEN_ID, user1, EXPIRATION + YEAR);
      await contract.connect(owner).setUser(TOKEN_ID, user2, 0);

      expect(await contract.userExpires(TOKEN_ID, user1)).to.equal(EXPIRATION + YEAR);
      expect(await contract.userExpires(TOKEN_ID, user2)).to.equal(0);
    });
  });

  describe("Events", function () {
    it("Should emit event when set user", async function () {
      const { contract, owner, user1 } = await loadFixture(deployContractFixture);

      await expect(contract.connect(owner).setUser(TOKEN_ID, user1, EXPIRATION))
        .to.emit(contract, "UpdateUser").withArgs(TOKEN_ID, user1.address, EXPIRATION);
    });
  });

});

参考实现

参考实现可在:ERC7507.sol 中找到:

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

import "./IERC7507.sol";

contract ERC7507 is ERC721, IERC7507 {

    mapping(uint256 => mapping(address => uint64)) private _expires;

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

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

    function userExpires(
        uint256 tokenId, address user
    ) public view virtual override returns(uint256) {
        require(_exists(tokenId), "ERC7507: query for nonexistent token");
        return _expires[tokenId][user];
    }

    function setUser(
        uint256 tokenId, address user, uint64 expires
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7507: caller is not owner or approved");
        _expires[tokenId][user] = expires;
        emit UpdateUser(tokenId, user, expires);
    }

}

安全注意事项

未发现安全注意事项。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang), "ERC-7507: 多用户 NFT 扩展 [DRAFT]," Ethereum Improvement Proposals, no. 7507, August 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7507.