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.
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:
supportsInterface(InterfaceID) check.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.
These are base terms used around the specification which function names and definitions are based on.
timestamp (seconds) representation of dates used for vesting.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 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 vs claimedPayout & claimablePayout
vestedPayout(uint256 tokenId) provides the total amount of payout tokens which have vested including 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:
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.
Generally in Solidity development it is advised against using block.timestamp as a state dependent 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 allotted by the vesting terms cannot be claimed.
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.These feature are not supported by the standard as is, but the standard could be extended to support these more advanced features.
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.This is done intentionally to keep the base standard simple. These features can and likely will be added through extensions of this standard.
EIP-721 compatibility, as well as Vesting NFT compatibility.The reference vesting NFT repository includes tests written in Hardhat.
A reference implementation of this EIP can be found in ERC-5725 assets.
timestamps
vestedPayoutAtTime(tokenId, type(uint256).max), for example, must return the total payout for a given tokenIdapprovals
Copyright and related rights waived via CC0.