A Non-Fungible Token (NFT) standard used to vest tokens (ERC-20 or otherwise) over a vesting release curve.
The following standard allows for the implementation of a standard API for NFT based contracts that hold and represent the vested and locked properties of any underlying token (ERC-20 or otherwise) that is emitted to the NFT holder. This standard is an extension of the ERC-721 token that provides basic functionality for creating vesting NFTs, claiming the tokens and reading vesting curve properties.
Motivation
Vesting contracts, including timelock contracts, lack a standard and unified interface, which results in diverse implementations of such contracts. Standardizing such contracts into a single interface would allow for the creation of an ecosystem of on- and off-chain tooling around these contracts. In addition, liquid vesting in the form of non-fungible assets can prove to be a huge improvement over traditional Simple Agreement for Future Tokens (SAFTs) or Externally Owned Account (EOA)-based vesting as it enables transferability and the ability to attach metadata similar to the existing functionality offered by with traditional NFTs.
Such a standard will not only provide a much-needed ERC-20 token lock standard, but will also enable the creation of secondary marketplaces tailored for semi-liquid SAFTs.
This standard also allows for a variety of different vesting curves to be implement easily.
These curves could represent:
linear vesting
cliff vesting
exponential vesting
custom deterministic vesting
Use Cases
A framework to release tokens over a set period of time that can be used to build many kinds of NFT financial products such as bonds, treasury bills, and many others.
Replicating SAFT contracts in a standardized form of semi-liquid vesting NFT assets.
SAFTs are generally off-chain, while today’s on-chain versions are mainly address-based, which makes distributing vesting shares to many representatives difficult. Standardization simplifies this convoluted process.
Providing a path for the standardization of vesting and token timelock contracts.
There are many such contracts in the wild and most of them differ in both interface and implementation.
NFT marketplaces dedicated to vesting NFTs.
Whole new sets of interfaces and analytics could be created from a common standard for token vesting NFTs.
Integrating vesting NFTs into services like Safe Wallet.
A standard would mean services like Safe Wallet could more easily and uniformly support interactions with these types of contracts inside of a multisig contract.
Enable standardized fundraising implementations and general fundraising that sell vesting tokens (eg. SAFTs) in a more transparent manner.
Allows tools, front-end apps, aggregators, etc. to show a more holistic view of the vesting tokens and the properties available to users.
Currently, every project needs to write their own visualization of the vesting schedule of their vesting assets. If this is standardized, third-party tools could be developed to aggregate all vesting NFTs from all projects for the user, display their schedules and allow the user to take aggregated vesting actions.
Such tooling can easily discover compliance through the ERC-165supportsInterface(InterfaceID) check.
Makes it easier for a single wrapping implementation to be used across all vesting standards that defines multiple recipients, periodic renting of vesting tokens etc.
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.
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.0;import"@openzeppelin/contracts/token/ERC721/IERC721.sol";/**
* @title Non-Fungible Vesting Token Standard.
* @notice A non-fungible token standard used to vest ERC-20 tokens over a vesting release curve
* scheduled using timestamps.
* @dev Because this standard relies on timestamps for the vesting schedule, it's important to keep track of the
* tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than allotted for a specific Vesting NFT.
* @custom:interface-id 0xbd3a202b
*/interfaceIERC5725isIERC721{/**
* This event is emitted when the payout is claimed through the claim function.
* @param tokenId the NFT tokenId of the assets being claimed.
* @param recipient The address which is receiving the payout.
* @param claimAmount The amount of tokens being claimed.
*/eventPayoutClaimed(uint256indexedtokenId,addressindexedrecipient,uint256claimAmount);/**
* This event is emitted when an `owner` sets an address to manage token claims for all tokens.
* @param owner The address setting a manager to manage all tokens.
* @param spender The address being permitted to manage all tokens.
* @param approved A boolean indicating whether the spender is approved to claim for all tokens.
*/eventClaimApprovalForAll(addressindexedowner,addressindexedspender,boolapproved);/**
* This event is emitted when an `owner` sets an address to manage token claims for a `tokenId`.
* @param owner The `owner` of `tokenId`.
* @param spender The address being permitted to manage a tokenId.
* @param tokenId The unique identifier of the token being managed.
* @param approved A boolean indicating whether the spender is approved to claim for `tokenId`.
*/eventClaimApproval(addressindexedowner,addressindexedspender,uint256indexedtokenId,boolapproved);/**
* @notice Claim the pending payout for the NFT.
* @dev MUST grant the claimablePayout value at the time of claim being called to `msg.sender`.
* MUST revert if not called by the token owner or approved users.
* MUST emit PayoutClaimed.
* SHOULD revert if there is nothing to claim.
* @param tokenId The NFT token id.
*/functionclaim(uint256tokenId)external;/**
* @notice Number of tokens for the NFT which have been claimed at the current timestamp.
* @param tokenId The NFT token id.
* @return payout The total amount of payout tokens claimed for this NFT.
*/functionclaimedPayout(uint256tokenId)externalviewreturns(uint256payout);/**
* @notice Number of tokens for the NFT which can be claimed at the current timestamp.
* @dev It is RECOMMENDED that this is calculated as the `vestedPayout()` subtracted from `payoutClaimed()`.
* @param tokenId The NFT token id.
* @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed.
*/functionclaimablePayout(uint256tokenId)externalviewreturns(uint256payout);/**
* @notice Total amount of tokens which have been vested at the current timestamp.
* This number also includes vested tokens which have been claimed.
* @dev It is RECOMMENDED that this function calls `vestedPayoutAtTime`
* with `block.timestamp` as the `timestamp` parameter.
* @param tokenId The NFT token id.
* @return payout Total amount of tokens which have been vested at the current timestamp.
*/functionvestedPayout(uint256tokenId)externalviewreturns(uint256payout);/**
* @notice Total amount of vested tokens at the provided timestamp.
* This number also includes vested tokens which have been claimed.
* @dev `timestamp` MAY be both in the future and in the past.
* Zero MUST be returned if the timestamp is before the token was minted.
* @param tokenId The NFT token id.
* @param timestamp The timestamp to check on, can be both in the past and the future.
* @return payout Total amount of tokens which have been vested at the provided timestamp.
*/functionvestedPayoutAtTime(uint256tokenId,uint256timestamp)externalviewreturns(uint256payout);/**
* @notice Number of tokens for an NFT which are currently vesting.
* @dev The sum of vestedPayout and vestingPayout SHOULD always be the total payout.
* @param tokenId The NFT token id.
* @return payout The number of tokens for the NFT which are vesting until a future date.
*/functionvestingPayout(uint256tokenId)externalviewreturns(uint256payout);/**
* @notice The start and end timestamps for the vesting of the provided NFT.
* MUST return the timestamp where no further increase in vestedPayout occurs for `vestingEnd`.
* @param tokenId The NFT token id.
* @return vestingStart The beginning of the vesting as a unix timestamp.
* @return vestingEnd The ending of the vesting as a unix timestamp.
*/functionvestingPeriod(uint256tokenId)externalviewreturns(uint256vestingStart,uint256vestingEnd);/**
* @notice Token which is used to pay out the vesting claims.
* @param tokenId The NFT token id.
* @return token The token which is used to pay out the vesting claims.
*/functionpayoutToken(uint256tokenId)externalviewreturns(addresstoken);/**
* @notice Sets a global `operator` with permission to manage all tokens owned by the current `msg.sender`.
* @param operator The address to let manage all tokens.
* @param approved A boolean indicating whether the spender is approved to claim for all tokens.
*/functionsetClaimApprovalForAll(addressoperator,boolapproved)external;/**
* @notice Sets a tokenId `operator` with permission to manage a single `tokenId` owned by the `msg.sender`.
* @param operator The address to let manage a single `tokenId`.
* @param tokenId the `tokenId` to be managed.
* @param approved A boolean indicating whether the spender is approved to claim for all tokens.
*/functionsetClaimApproval(addressoperator,boolapproved,uint256tokenId)external;/**
* @notice Returns true if `owner` has set `operator` to manage all `tokenId`s.
* @param owner The owner allowing `operator` to manage all `tokenId`s.
* @param operator The address who is given permission to spend tokens on behalf of the `owner`.
*/functionisClaimApprovedForAll(addressowner,addressoperator)externalviewreturns(boolisClaimApproved);/**
* @notice Returns the operating address for a `tokenId`.
* If `tokenId` is not managed, then returns the zero address.
* @param tokenId The NFT `tokenId` to query for a `tokenId` manager.
*/functiongetClaimApproved(uint256tokenId)externalviewreturns(addressoperator);}
Rationale
Terms
These are base terms used around the specification which function names and definitions are based on.
vesting: Tokens which a vesting NFT is vesting until a future date.
vested: Total amount of tokens a vesting NFT has vested.
claimable: Amount of vested tokens which can be unlocked.
claimed: Total amount of tokens unlocked from a vesting NFT.
timestamp: The unix timestamp (seconds) representation of dates used for vesting.
Vesting Functions
vestingPayout + vestedPayout
vestingPayout(uint256 tokenId) and vestedPayout(uint256 tokenId) add up to the total number of tokens which can be claimed by the end of of the vesting schedule. This is also equal to vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) with type(uint256).max as the timestamp.
The rationale for this is to guarantee that the tokens vested and tokens vesting are always in sync. The intent is that the vesting curves created are deterministic across the vestingPeriod. This creates useful opportunities for integration with these NFTs. For example: A vesting schedule can be iterated through and a vesting curve could be visualized, either on-chain or off-chain.
vestedPayout(uint256 tokenId) provides the total amount of payout tokens which have vestedincluding claimedPayout(uint256 tokenId).
claimedPayout(uint256 tokenId) provides the total amount of payout tokens which have been unlocked at the current timestamp.
claimablePayout(uint256 tokenId) provides the amount of payout tokens which can be unlocked at the current timestamp.
The rationale for providing three functions is to support a number of features:
The return of vestedPayout(uint256 tokenId) will always match the return of vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) with block.timestamp as the timestamp.
claimablePayout(uint256 tokenId) can be used to easily see the current payout unlock amount and allow for unlock cliffs by returning zero until a timestamp has been passed.
claimedPayout(uint256 tokenId) is helpful to see tokens unlocked from an NFT and it is also necessary for the calculation of vested-but-locked payout tokens: vestedPayout - claimedPayout - claimablePayout = lockedPayout. This would depend on how the vesting curves are configured by the an implementation of this standard.
vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) provides functionality to iterate through the vestingPeriod(uint256 tokenId) and provide a visual of the release curve. The intent is that release curves are created which makes vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) deterministic.
Timestamps
Generally in Solidity development it is advised against using block.timestamp as a state dependant variable as the timestamp of a block can be manipulated by a miner. The choice to use a timestamp over a block is to allow the interface to work across multiple Ethereum Virtual Machine (EVM) compatible networks which generally have different block times. Block proposal with a significantly fabricated timestamp will generally be dropped by all node implementations which makes the window for abuse negligible.
The timestamp makes cross chain integration easy, but internally, the reference implementation keeps track of the token payout per Vesting NFT to ensure that excess tokens alloted by the vesting terms cannot be claimed.
Limitation of Scope
Historical claims: While historical vesting schedules can be determined on-chain with vestedPayoutAtTime(uint256 tokenId, uint256 timestamp), historical claims would need to be calculated through historical transaction data. Most likely querying for PayoutClaimed events to build a historical graph.
Extension Possibilities
These feature are not supported by the standard as is, but the standard could be extended to support these more advanced features.
Custom Vesting Curves: This standard intends on returning deterministic vesting values given NFT tokenId and a timestamp as inputs. This is intentional as it provides for flexibility in how the vesting curves work under the hood which doesn’t constrain projects who intend on building a complex smart contract vesting architecture.
NFT Rentals: Further complex DeFi tools can be created if vesting NFTs could be rented.
This is done intentionally to keep the base standard simple. These features can and likely will be added through extensions of this standard.
Backwards Compatibility
The Vesting NFT standard is meant to be fully backwards compatible with any current ERC-721 integrations and marketplaces.
The Vesting NFT standard also supports ERC-165 interface detection for detecting EIP-721 compatibility, as well as Vesting NFT compatibility.
Test Cases
The reference vesting NFT repository includes tests written in Hardhat.
Reference Implementation
A reference implementation of this EIP can be found in ERC-5725 assets.
Security Considerations
timestamps
Vesting schedules are based on timestamps. As such, it’s important to keep track of the number of tokens which have been claimed and to not give out more tokens than alloted for a specific Vesting NFT.
vestedPayoutAtTime(tokenId, type(uint256).max), for example, must return the total payout for a given tokenId
approvals
When an ERC-721 approval is made on a Vesting NFT, the operator would have the rights to transfer the Vesting NFT to themselves and then claim the vested tokens.
When a ERC-5725 approval is made on a Vesting NFT, the operator would have the rights to claim the vested tokens, but not transfer the NFT away from the owner.