This standard is an extension of EIP-721. It proposes an additional interface for NFTs to be used as recurring, expirable subscriptions. The interface includes functions to renew and cancel the subscription.
Motivation
NFTs are commonly used as accounts on decentralized apps or membership passes to communities, events, and more. However, it is currently rare to see NFTs like these that have a finite expiration date. The “permanence” of the blockchain often leads to memberships that have no expiration dates and thus no required recurring payments. However, for many real-world applications, a paid subscription is needed to keep an account or membership valid.
The most prevalent on-chain application that makes use of the renewable subscription model is the Ethereum Name Service (ENS), which utilizes a similar interface to the one proposed below. Each domain can be renewed for a certain period of time, and expires if payments are no longer made. A common interface will make it easier for future projects to develop subscription-based NFTs. In the current Web2 world, it’s hard for a user to see or manage all of their subscriptions in one place. With a common standard for subscriptions, it will be easy for a single application to determine the number of subscriptions a user has, see when they expire, and renew/cancel them as requested.
Additionally, as the prevalence of secondary royalties from NFT trading disappears, creators will need new models for generating recurring income. For NFTs that act as membership or access passes, pivoting to a subscription-based model is one way to provide income and also force issuers to keep providing value.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
interfaceIERC5643{/// @notice Emitted when a subscription expiration changes
/// @dev When a subscription is canceled, the expiration value should also be 0.
eventSubscriptionUpdate(uint256indexedtokenId,uint64expiration);/// @notice Renews the subscription to an NFT
/// Throws if `tokenId` is not a valid NFT
/// @param tokenId The NFT to renew the subscription for
/// @param duration The number of seconds to extend a subscription for
functionrenewSubscription(uint256tokenId,uint64duration)externalpayable;/// @notice Cancels the subscription of an NFT
/// @dev Throws if `tokenId` is not a valid NFT
/// @param tokenId The NFT to cancel the subscription for
functioncancelSubscription(uint256tokenId)externalpayable;/// @notice Gets the expiration date of a subscription
/// @dev Throws if `tokenId` is not a valid NFT
/// @param tokenId The NFT to get the expiration date of
/// @return The expiration date of the subscription
functionexpiresAt(uint256tokenId)externalviewreturns(uint64);/// @notice Determines whether a subscription can be renewed
/// @dev Throws if `tokenId` is not a valid NFT
/// @param tokenId The NFT to get the expiration date of
/// @return The renewability of a the subscription
functionisRenewable(uint256tokenId)externalviewreturns(bool);}
The expiresAt(uint256 tokenId) function MAY be implemented as pure or view.
The isRenewable(uint256 tokenId) function MAY be implemented as pure or view.
The renewSubscription(uint256 tokenId, uint64 duration) function MAY be implemented as external or public.
The cancelSubscription(uint256 tokenId) function MAY be implemented as external or public.
The SubscriptionUpdate event MUST be emitted whenever the expiration date of a subscription is changed.
The supportsInterface method MUST return true when called with 0x8c65f84d.
Rationale
This standard aims to make on-chain subscriptions as simple as possible by adding the minimal required functions and events for implementing on-chain subscriptions. It is important to note that in this interface, the NFT itself represents ownership of a subscription, there is no facilitation of any other fungible or non-fungible tokens.
Subscription Management
Subscriptions represent agreements to make advanced payments in order to receive or participate in something. In order to facilitate these agreements, a user must be able to renew or cancel their subscriptions hence the renewSubscription and cancelSubscription functions. It also important to know when a subscription expires - users will need this information to know when to renew, and applications need this information to determine the validity of a subscription NFT. The expiresAt function provides this functionality. Finally, it is possible that a subscription may not be renewed once expired. The isRenewable function gives users and applications that information.
Easy Integration
Because this standard is fully EIP-721 compliant, existing protocols will be able to facilitate the transfer of subscription NFTs out of the box. With only a few functions to add, protocols will be able to fully manage a subscription’s expiration, determine whether a subscription is expired, and see whether it can be renewed.
Backwards Compatibility
This standard can be fully EIP-721 compatible by adding an extension function set.
The new functions introduced in this standard add minimal overhead to the existing EIP-721 interface, which should make adoption straightforward and quick for developers.
Test Cases
The following tests require Foundry.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.13;import"forge-std/Test.sol";import"../src/ERC5643.sol";contractERC5643MockisERC5643{constructor(stringmemoryname_,stringmemorysymbol_)ERC5643(name_,symbol_){}functionmint(addressto,uint256tokenId)public{_mint(to,tokenId);}}contractERC5643TestisTest{eventSubscriptionUpdate(uint256indexedtokenId,uint64expiration);addressuser1;uint256tokenId;ERC5643Mockerc5643;functionsetUp()public{tokenId=1;user1=address(0x1);erc5643=newERC5643Mock("erc5369","ERC5643");erc5643.mint(user1,tokenId);}functiontestRenewalValid()public{vm.warp(1000);vm.prank(user1);vm.expectEmit(true,true,false,true);emitSubscriptionUpdate(tokenId,3000);erc5643.renewSubscription(tokenId,2000);}functiontestRenewalNotOwner()public{vm.expectRevert("Caller is not owner nor approved");erc5643.renewSubscription(tokenId,2000);}functiontestCancelValid()public{vm.prank(user1);vm.expectEmit(true,true,false,true);emitSubscriptionUpdate(tokenId,0);erc5643.cancelSubscription(tokenId);}functiontestCancelNotOwner()public{vm.expectRevert("Caller is not owner nor approved");erc5643.cancelSubscription(tokenId);}functiontestExpiresAt()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);}}
Reference Implementation
Implementation: ERC5643.sol
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.13;import"@openzeppelin/contracts/token/ERC721/ERC721.sol";import"./IERC5643.sol";contractERC5643isERC721,IERC5643{mapping(uint256=>uint64)private_expirations;constructor(stringmemoryname_,stringmemorysymbol_)ERC721(name_,symbol_){}functionrenewSubscription(uint256tokenId,uint64duration)externalpayable{require(_isApprovedOrOwner(msg.sender,tokenId),"Caller is not owner nor approved");uint64currentExpiration=_expirations[tokenId];uint64newExpiration;if(currentExpiration==0){newExpiration=uint64(block.timestamp)+duration;}else{if(!_isRenewable(tokenId)){revertSubscriptionNotRenewable();}newExpiration=currentExpiration+duration;}_expirations[tokenId]=newExpiration;emitSubscriptionUpdate(tokenId,newExpiration);}functioncancelSubscription(uint256tokenId)externalpayable{require(_isApprovedOrOwner(msg.sender,tokenId),"Caller is not owner nor approved");delete_expirations[tokenId];emitSubscriptionUpdate(tokenId,0);}functionexpiresAt(uint256tokenId)externalviewreturns(uint64){return_expirations[tokenId];}functionisRenewable(uint256tokenId)externalpurereturns(bool){returntrue;}functionsupportsInterface(bytes4interfaceId)publicviewvirtualoverridereturns(bool){returninterfaceId==type(IERC5643).interfaceId||super.supportsInterface(interfaceId);}}
Security Considerations
This EIP standard does not affect ownership of an NFT and thus can be considered secure.