Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-5643: 订阅 NFT

为 EIP-721 代币添加基于订阅的功能

Authors cygaar (@cygaar)
Created 2022-09-10
Discussion Link https://ethereum-magicians.org/t/eip-5643-subscription-nfts/10802
Requires EIP-721

摘要

本标准是 EIP-721 的扩展。它为 NFT 提出了一个额外的接口,用作循环的、可过期的订阅。该接口包括更新和取消订阅的功能。

动机

NFT 通常用作去中心化应用程序上的帐户或社区、活动等的会员通行证。但是,目前很少看到像这样具有有限到期日期的 NFT。区块链的“永久性”通常会导致会员资格没有到期日期,因此也不需要经常性付款。但是,对于许多现实世界的应用程序来说,需要付费订阅才能保持帐户或会员资格有效。

使用可续订订阅模型的最流行的链上应用程序是以太坊名称服务(ENS),它使用类似于下面提出的接口。每个域名都可以续订一段时间,如果不再付款则会过期。一个通用的接口将使未来的项目更容易开发基于订阅的 NFT。在当前的 Web2 世界中,用户很难在一个地方查看或管理他们所有的订阅。有了通用的订阅标准,单个应用程序可以很容易地确定用户拥有的订阅数量,查看它们的到期时间,并根据要求续订/取消它们。

此外,随着 NFT 交易中二级版税的消失,创作者将需要新的模式来产生经常性收入。对于充当会员或访问通行证的 NFT 来说,转向基于订阅的模式是提供收入的一种方式,也可以迫使发行者不断提供价值。

规范

本文档中的关键词“必须(MUST)”、“禁止(MUST NOT)”、“必需(REQUIRED)”、“应该(SHALL)”、“不应该(SHALL NOT)”、“推荐(SHOULD)”、“不推荐(SHOULD NOT)”、“可以(MAY)”和“可选(OPTIONAL)”应按照 RFC 2119 中的描述进行解释。

interface IERC5643 {
    /// @notice 当订阅到期日更改时发出
    /// @dev 当订阅被取消时,到期值也应该为 0。
    event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration);

    /// @notice 续订 NFT 的订阅
    /// 如果 `tokenId` 不是有效的 NFT,则抛出异常
    /// @param tokenId 要续订订阅的 NFT
    /// @param duration 延长订阅的秒数
    function renewSubscription(uint256 tokenId, uint64 duration) external payable;

    /// @notice 取消 NFT 的订阅
    /// @dev 如果 `tokenId` 不是有效的 NFT,则抛出异常
    /// @param tokenId 要取消订阅的 NFT
    function cancelSubscription(uint256 tokenId) external payable;

    /// @notice 获取订阅的到期日期
    /// @dev 如果 `tokenId` 不是有效的 NFT,则抛出异常
    /// @param tokenId 要获取到期日期的 NFT
    /// @return 订阅的到期日期
    function expiresAt(uint256 tokenId) external view returns(uint64);

    /// @notice 确定订阅是否可以续订
    /// @dev 如果 `tokenId` 不是有效的 NFT,则抛出异常
    /// @param tokenId 要获取到期日期的 NFT
    /// @return 订阅的可续订性
    function isRenewable(uint256 tokenId) external view returns(bool);
}

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

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

renewSubscription(uint256 tokenId, uint64 duration) 函数可以实现为 externalpublic

cancelSubscription(uint256 tokenId) 函数可以实现为 externalpublic

每当订阅的到期日期更改时,必须发出 SubscriptionUpdate 事件。

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

原理

本标准旨在通过添加实现链上订阅所需的最少功能和事件来尽可能简化链上订阅。重要的是要注意,在此接口中,NFT 本身代表订阅的所有权,不便于任何其他同质化或非同质化代币。

订阅管理

订阅表示为了接收或参与某些内容而进行预付款的协议。为了促进这些协议,用户必须能够续订或取消他们的订阅,因此有了 renewSubscriptioncancelSubscription 函数。了解订阅何时到期也很重要 - 用户需要此信息来了解何时续订,应用程序需要此信息来确定订阅 NFT 的有效性。expiresAt 函数提供了此功能。最后,订阅可能在过期后无法续订。isRenewable 函数为用户和应用程序提供了该信息。

易于集成

由于此标准完全符合 EIP-721,因此现有协议将能够立即促进订阅 NFT 的转移。只需添加几个函数,协议就可以完全管理订阅的到期时间,确定订阅是否已过期,并查看是否可以续订。

向后兼容性

通过添加扩展函数集,此标准可以完全与 EIP-721 兼容。

本标准中引入的新函数对现有的 EIP-721 接口增加了最小的开销,这应该使开发人员能够直接快速地采用。

测试用例

以下测试需要 Foundry。

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

import "forge-std/Test.sol";
import "../src/ERC5643.sol";

contract ERC5643Mock is ERC5643 {
    constructor(string memory name_, string memory symbol_) ERC5643(name_, symbol_) {}

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

contract ERC5643Test is Test {
    event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration);

    address user1;
    uint256 tokenId;
    ERC5643Mock erc5643;

    function setUp() public {
        tokenId = 1;
        user1 = address(0x1);

        erc5643 = new ERC5643Mock("erc5369", "ERC5643");
        erc5643.mint(user1, tokenId);
    }

    function testRenewalValid() public {
        vm.warp(1000);
        vm.prank(user1);
        vm.expectEmit(true, true, false, true);
        emit SubscriptionUpdate(tokenId, 3000);
        erc5643.renewSubscription(tokenId, 2000);
    }

    function testRenewalNotOwner() public {
        vm.expectRevert("Caller is not owner nor approved");
        erc5643.renewSubscription(tokenId, 2000);
    }

    function testCancelValid() public {
        vm.prank(user1);
        vm.expectEmit(true, true, false, true);
        emit SubscriptionUpdate(tokenId, 0);
        erc5643.cancelSubscription(tokenId);
    }

    function testCancelNotOwner() public {
        vm.expectRevert("Caller is not owner nor approved");
        erc5643.cancelSubscription(tokenId);
    }

    function testExpiresAt() public {
        vm.warp(1000);

        assertEq(erc5643.expiresAt(tokenId), 0);
        vm.startPrank(user1);
        erc5643.renewSubscription(tokenId, 2000);
        assertEq(erc5643.expiresAt(tokenId), 3000);

        erc5643.cancelSubscription(tokenId);
        assertEq(erc5643.expiresAt(tokenId), 0);
    }
}

参考实现

实现: ERC5643.sol

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

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

contract ERC5643 is ERC721, IERC5643 {
    mapping(uint256 => uint64) private _expirations;

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

    function renewSubscription(uint256 tokenId, uint64 duration) external payable {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved");

        uint64 currentExpiration = _expirations[tokenId];
        uint64 newExpiration;
        if (currentExpiration == 0) {
            newExpiration = uint64(block.timestamp) + duration;
        } else {
            if (!_isRenewable(tokenId)) {
                revert SubscriptionNotRenewable();
            }
            newExpiration = currentExpiration + duration;
        }

        _expirations[tokenId] = newExpiration;

        emit SubscriptionUpdate(tokenId, newExpiration);
    }

    function cancelSubscription(uint256 tokenId) external payable {
        require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved");
        delete _expirations[tokenId];
        emit SubscriptionUpdate(tokenId, 0);
    }

    function expiresAt(uint256 tokenId) external view returns(uint64) {
        return _expirations[tokenId];
    }

    function isRenewable(uint256 tokenId) external pure returns(bool) {
        return true;
    }

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

安全考虑

此 EIP 标准不影响 NFT 的所有权,因此可以认为是安全的。

版权

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

Citation

Please cite this document as:

cygaar (@cygaar), "ERC-5643: 订阅 NFT [DRAFT]," Ethereum Improvement Proposals, no. 5643, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5643.