From 70bb638c6cd38691c13cd02c49ac8a05b8654838 Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 28 Sep 2022 10:46:58 +0800 Subject: [PATCH 01/22] chore: Commit draft EIP --- EIPS/eip-draft.md | 225 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 EIPS/eip-draft.md diff --git a/EIPS/eip-draft.md b/EIPS/eip-draft.md new file mode 100644 index 00000000000000..21519d4eb1bf00 --- /dev/null +++ b/EIPS/eip-draft.md @@ -0,0 +1,225 @@ +--- +eip: +title: Transferable Vesting NFT standard +description: A standard for transferable vesting NFTs which release underlying tokens (ERC-20 or otherwise) over time. +# TODO: Fill in proper org and user names +author: Apeguru (https://github.com/Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (https://github.com/DeFiFoFum) +# TODO: Add in discussion URL +discussions-to: +status: Draft +type: Standards Track +category: ERC +created: 2022-09-08 +requires: 721 +--- +# EIP-XXXX Vesting NFT Standard + +## Table of Contents +- [EIP-XXXX Vesting NFT Standard](#eip-xxxx-vesting-nft-standard) + - [Table of Contents](#table-of-contents) + - [Simple Summary](#simple-summary) + - [Abstract](#abstract) + - [Motivation](#motivation) + - [Use Cases](#use-cases) + - [Specification](#specification) + - [Rationale](#rationale) + - [Backwards Compatibility](#backwards-compatibility) + - [Reference Implementation](#reference-implementation) + - [Test Cases](#test-cases) + - [Security Considerations](#security-considerations) + - [Extensions](#extensions) + - [References](#references) + - [Copyright](#copyright) + - [Citation](#citation) + + +## Simple Summary +A **Non-Fungible Token** (NFT) standard used to vest tokens (ERC-20 or otherwise) over a vesting release curve. + +## Abstract +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 of this, 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 +1. 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. +2. 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. +3. 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. +4. NFT marketplaces dedicated to vesting NFTs + - Whole new sets of interfaces and analytics could be created from a common standard for token vesting NFTs. +5. Integrating vesting NFTs into services like Gnosis Safe + - A standard would mean services like Gnosis Safe could more easily and uniformly support interactions with these types of contracts inside of a multisig contract. +6. Enable standardized fundraising implementations and general fundraising that sell vesting tokens (eg. SAFTs) in a more transparent manner. +7. 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 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 EIP-165 supportsInterface(IVestingNFT) check. +8. 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. + +```solidity +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^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 tokens (ERC-20 or otherwise) 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 alloted for a specific Vesting NFT. + */ +interface IVestingNFT is IERC721 { + event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); + + /** + * @notice Claim the pending payout for the NFT + * @dev MUST grant the claimablePayout value at the time of claim being called + * MUST revert if not called by the token owner or approved users + * SHOULD revert if there is nothing to claim + * @param tokenId The NFT token id + * @return amountClaimed The amount of tokens claimed in this call + */ + function claim(uint256 tokenId) external returns (uint256 amountClaimed); + + /** + * @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. + */ + function vestedPayout(uint256 tokenId) external view returns (uint256 payout); + + /** + * @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 + */ + function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout); + + /** + * @notice Number of tokens for an NFT which are currently vesting (locked). + * @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 have not been claimed yet, + * regardless of whether they are ready to claim + */ + function vestingPayout(uint256 tokenId) external view returns (uint256 payout); + + /** + * @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()` value with the total + * amount of tokens claimed subtracted. + * @param tokenId The NFT token id + * @return payout The number of vested tokens for the NFT which have not been claimed yet + */ + function claimablePayout(uint256 tokenId) external view returns (uint256 payout); + + /** + * @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 + */ + function vestingPeriod(uint256 tokenId) external view returns (uint256 vestingStart, uint256 vestingEnd); + + /** + * @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 + */ + function payoutToken(uint256 tokenId) external view returns (address token); +} +``` + + +## Rationale + +**vesting terms** +- _vesting_: Tokens which are locked until a future date +- _vested_: Tokens which have reached their unlock date. (The usage in this specification relates to the **total** vested tokens for a given Vesting NFT.) +- _claimable_: Amount of tokens which can be claimed at the current `timestamp`. +- _timestamp_: The [unix](https://www.unixtimestamp.com/) `timestamp` (seconds) representation of dates used for vesting. + +**vesting functions** +- `vestingPayout()` and `vestedPayout()` add up to the total number of tokens which can be claimed by the end of of the vesting schedule, which is also equal to `vestedPayoutAtTime()` with `type(uint256).max` as the `timestamp`. +- `vestedPayout()` will provide the total amount of tokens which are eligible for release (including claimed tokens), while `claimablePayout()` provides the amount of tokens which can be claimed at the current `timestamp`. +- `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. It would be incredible to see integrations such as [hot-chain-svg](https://github.com/w1nt3r-eth/hot-chain-svg) to be able to create SVG visuals of vesting curves directly on-chain. + + + +**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. + + +## Backwards Compatibility + +- The Vesting NFT standard is meant to be fully backwards compatible with any current [ERC-721 standard](https://eips.ethereum.org/EIPS/eip-721) integrations and marketplaces. +- The Vesting NFT standard also supports [ERC-165 standard](https://eips.ethereum.org/EIPS/eip-165) interface detection for detecting `ERC-721` compatibility, as well as Vesting NFT compatibility. + +## Reference Implementation + + +A reference implementation of this EIP can be found in [this repository](https://github.com/ApeSwapFinance/eip-xxxx-vesting-nft-implementation). + + +## Test Cases +The reference vesting NFT repository includes tests written in Hardhat. + + +## 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 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. + +## Extensions +- Vesting Curves +- Rental +- Beneficiary + +## References +Standards + +- [ERC-20](https://eips.ethereum.org/EIPS/eip-20) Token Standard. +- [ERC-165](https://eips.ethereum.org/EIPS/eip-165) Standard Interface Detection. +- [ERC-721](https://eips.ethereum.org/EIPS/eip-721) Token Standard. + +- [Timestamp Dependence](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/#the-15-second-rule) The 15-second Rule. +- [hot-chain-svg](https://github.com/w1nt3r-eth/hot-chain-svg) On-chain SVG generator. Could be used to generate vesting curves for Vesting NFTs on-chain. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). + +## Citation +Please cite this document as: + +Apeguru, Marco, Mario, DeFiFoFum, "EIP-XXXX: Vesting NFT Standard," Ethereum Improvement Proposals, no. XXXX, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-XXXX. \ No newline at end of file From bfc1e1f7a217ec1a2b7fe266f7423a3d9bcda38b Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 28 Sep 2022 10:56:01 +0800 Subject: [PATCH 02/22] chore: ERC to EIP and add EIP number --- EIPS/{eip-draft.md => eip-5725.md} | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) rename EIPS/{eip-draft.md => eip-5725.md} (93%) diff --git a/EIPS/eip-draft.md b/EIPS/eip-5725.md similarity index 93% rename from EIPS/eip-draft.md rename to EIPS/eip-5725.md index 21519d4eb1bf00..84b20603012738 100644 --- a/EIPS/eip-draft.md +++ b/EIPS/eip-5725.md @@ -1,7 +1,7 @@ --- -eip: +eip: 5725 title: Transferable Vesting NFT standard -description: A standard for transferable vesting NFTs which release underlying tokens (ERC-20 or otherwise) over time. +description: A standard for transferable vesting NFTs which release underlying tokens (EIP-20 or otherwise) over time. # TODO: Fill in proper org and user names author: Apeguru (https://github.com/Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (https://github.com/DeFiFoFum) # TODO: Add in discussion URL @@ -12,10 +12,10 @@ category: ERC created: 2022-09-08 requires: 721 --- -# EIP-XXXX Vesting NFT Standard +# EIP-5725 Vesting NFT Standard ## Table of Contents -- [EIP-XXXX Vesting NFT Standard](#eip-xxxx-vesting-nft-standard) +- [EIP-5725 Vesting NFT Standard](#eip-5725-vesting-nft-standard) - [Table of Contents](#table-of-contents) - [Simple Summary](#simple-summary) - [Abstract](#abstract) @@ -34,15 +34,15 @@ requires: 721 ## Simple Summary -A **Non-Fungible Token** (NFT) standard used to vest tokens (ERC-20 or otherwise) over a vesting release curve. +A **Non-Fungible Token** (NFT) standard used to vest tokens (EIP-20 or otherwise) over a vesting release curve. ## Abstract -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. +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 (EIP-20 or otherwise) that is emitted to the NFT holder. This standard is an extension of the EIP-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 of this, 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. +Such a standard will not only provide a much-needed EIP-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. @@ -80,7 +80,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; /** * @title Non-Fungible Vesting Token Standard - * @notice A non-fungible token standard used to vest tokens (ERC-20 or otherwise) over a vesting release curve + * @notice A non-fungible token standard used to vest tokens (EIP-20 or otherwise) 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 alloted for a specific Vesting NFT. @@ -179,13 +179,13 @@ The `timestamp` makes cross chain integration easy, but internally, the referenc ## Backwards Compatibility -- The Vesting NFT standard is meant to be fully backwards compatible with any current [ERC-721 standard](https://eips.ethereum.org/EIPS/eip-721) integrations and marketplaces. -- The Vesting NFT standard also supports [ERC-165 standard](https://eips.ethereum.org/EIPS/eip-165) interface detection for detecting `ERC-721` compatibility, as well as Vesting NFT compatibility. +- The Vesting NFT standard is meant to be fully backwards compatible with any current [EIP-721 standard](https://eips.ethereum.org/EIPS/eip-721) integrations and marketplaces. +- The Vesting NFT standard also supports [EIP-165 standard](https://eips.ethereum.org/EIPS/eip-165) interface detection for detecting `EIP-721` compatibility, as well as Vesting NFT compatibility. ## Reference Implementation -A reference implementation of this EIP can be found in [this repository](https://github.com/ApeSwapFinance/eip-xxxx-vesting-nft-implementation). +A reference implementation of this EIP can be found in [this repository](https://github.com/ApeSwapFinance/eip-5725-vesting-nft-implementation). ## Test Cases @@ -209,9 +209,9 @@ The reference vesting NFT repository includes tests written in Hardhat. ## References Standards -- [ERC-20](https://eips.ethereum.org/EIPS/eip-20) Token Standard. -- [ERC-165](https://eips.ethereum.org/EIPS/eip-165) Standard Interface Detection. -- [ERC-721](https://eips.ethereum.org/EIPS/eip-721) Token Standard. +- [EIP-20](https://eips.ethereum.org/EIPS/eip-20) Token Standard. +- [EIP-165](https://eips.ethereum.org/EIPS/eip-165) Standard Interface Detection. +- [EIP-721](https://eips.ethereum.org/EIPS/eip-721) Token Standard. - [Timestamp Dependence](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/#the-15-second-rule) The 15-second Rule. - [hot-chain-svg](https://github.com/w1nt3r-eth/hot-chain-svg) On-chain SVG generator. Could be used to generate vesting curves for Vesting NFTs on-chain. From 1d9961ab36413053a69a7c7b65f87bc411d2b5fd Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 28 Sep 2022 11:09:07 +0800 Subject: [PATCH 03/22] refactor: EIP Validator changes --- EIPS/eip-5725.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 84b20603012738..7407e9c71ea2b9 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -1,10 +1,8 @@ --- eip: 5725 -title: Transferable Vesting NFT standard -description: A standard for transferable vesting NFTs which release underlying tokens (EIP-20 or otherwise) over time. -# TODO: Fill in proper org and user names -author: Apeguru (https://github.com/Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (https://github.com/DeFiFoFum) -# TODO: Add in discussion URL +title: Transferable Vesting NFT +description: An interface for transferable vesting NFTs which release underlying tokens over time. +author: Apeguru (@Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (https://github.com/DeFiFoFum) discussions-to: status: Draft type: Standards Track @@ -12,10 +10,11 @@ category: ERC created: 2022-09-08 requires: 721 --- -# EIP-5725 Vesting NFT Standard + +# EIP-5725 Vesting NFT ## Table of Contents -- [EIP-5725 Vesting NFT Standard](#eip-5725-vesting-nft-standard) +- [EIP-5725 Vesting NFT](#eip-5725-vesting-nft) - [Table of Contents](#table-of-contents) - [Simple Summary](#simple-summary) - [Abstract](#abstract) @@ -182,15 +181,12 @@ The `timestamp` makes cross chain integration easy, but internally, the referenc - The Vesting NFT standard is meant to be fully backwards compatible with any current [EIP-721 standard](https://eips.ethereum.org/EIPS/eip-721) integrations and marketplaces. - The Vesting NFT standard also supports [EIP-165 standard](https://eips.ethereum.org/EIPS/eip-165) interface detection for detecting `EIP-721` compatibility, as well as Vesting NFT compatibility. -## Reference Implementation - - -A reference implementation of this EIP can be found in [this repository](https://github.com/ApeSwapFinance/eip-5725-vesting-nft-implementation). - - ## Test Cases The reference vesting NFT repository includes tests written in Hardhat. +## Reference Implementation + +A reference implementation of this EIP can be found in [this repository](https://github.com/ApeSwapFinance/eip-5725-vesting-nft-implementation). ## Security Considerations @@ -221,5 +217,4 @@ Copyright and related rights waived via [CC0](../LICENSE.md). ## Citation Please cite this document as: - -Apeguru, Marco, Mario, DeFiFoFum, "EIP-XXXX: Vesting NFT Standard," Ethereum Improvement Proposals, no. XXXX, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-XXXX. \ No newline at end of file +Apeguru(@Apegurus), Marco, Mario, DeFiFoFum, "EIP-5725: Vesting NFT," Ethereum Improvement Proposals, no. XXXX, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-XXXX. \ No newline at end of file From 81feac087fcaa66a917b3690da7fcbecc05c37c8 Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 28 Sep 2022 11:20:17 +0800 Subject: [PATCH 04/22] refactor: Relative EIP linking --- EIPS/eip-5725.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 7407e9c71ea2b9..bdb9ef25b37288 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -2,7 +2,7 @@ eip: 5725 title: Transferable Vesting NFT description: An interface for transferable vesting NFTs which release underlying tokens over time. -author: Apeguru (@Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (https://github.com/DeFiFoFum) +author: Apeguru (@Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (@DeFiFoFum) discussions-to: status: Draft type: Standards Track @@ -11,7 +11,7 @@ created: 2022-09-08 requires: 721 --- -# EIP-5725 Vesting NFT +# [EIP-5725](./eip-5725.md) Vesting NFT ## Table of Contents - [EIP-5725 Vesting NFT](#eip-5725-vesting-nft) @@ -33,15 +33,15 @@ requires: 721 ## Simple Summary -A **Non-Fungible Token** (NFT) standard used to vest tokens (EIP-20 or otherwise) over a vesting release curve. +A **Non-Fungible Token** (NFT) standard used to vest tokens ([EIP-20](./eip-20.md) or otherwise) over a vesting release curve. ## Abstract -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 (EIP-20 or otherwise) that is emitted to the NFT holder. This standard is an extension of the EIP-721 token that provides basic functionality for creating vesting NFTs, claiming the tokens and reading vesting curve properties. +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 ([EIP-20](./eip-20.md) or otherwise) that is emitted to the NFT holder. This standard is an extension of the [EIP-721](./eip-721.md) 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 of this, 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 EIP-20 token lock standard, but will also enable the creation of secondary marketplaces tailored for semi-liquid SAFTs. +Such a standard will not only provide a much-needed [EIP-20](./eip-20.md) 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. @@ -64,7 +64,7 @@ These curves could represent: 6. Enable standardized fundraising implementations and general fundraising that sell vesting tokens (eg. SAFTs) in a more transparent manner. 7. 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 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 EIP-165 supportsInterface(IVestingNFT) check. + - Such tooling can easily discover compliance through the [EIP-165](./eip-165.md) supportsInterface(IVestingNFT) check. 8. 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. @@ -178,8 +178,8 @@ The `timestamp` makes cross chain integration easy, but internally, the referenc ## Backwards Compatibility -- The Vesting NFT standard is meant to be fully backwards compatible with any current [EIP-721 standard](https://eips.ethereum.org/EIPS/eip-721) integrations and marketplaces. -- The Vesting NFT standard also supports [EIP-165 standard](https://eips.ethereum.org/EIPS/eip-165) interface detection for detecting `EIP-721` compatibility, as well as Vesting NFT compatibility. +- The Vesting NFT standard is meant to be fully backwards compatible with any current [EIP-721 standard](./eip-721.md) integrations and marketplaces. +- The Vesting NFT standard also supports [EIP-165 standard](./eip-165.md) 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. @@ -205,9 +205,9 @@ A reference implementation of this EIP can be found in [this repository](https:/ ## References Standards -- [EIP-20](https://eips.ethereum.org/EIPS/eip-20) Token Standard. -- [EIP-165](https://eips.ethereum.org/EIPS/eip-165) Standard Interface Detection. -- [EIP-721](https://eips.ethereum.org/EIPS/eip-721) Token Standard. +- [EIP-20](./eip-20.md) Token Standard. +- [EIP-165](./eip-165.md) Standard Interface Detection. +- [EIP-721](./eip-721.md) Token Standard. - [Timestamp Dependence](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/#the-15-second-rule) The 15-second Rule. - [hot-chain-svg](https://github.com/w1nt3r-eth/hot-chain-svg) On-chain SVG generator. Could be used to generate vesting curves for Vesting NFTs on-chain. From e05139067be7f0c5069641b7de3fa31b144895a5 Mon Sep 17 00:00:00 2001 From: MarcoPaladin Date: Fri, 30 Sep 2022 15:43:28 +0200 Subject: [PATCH 05/22] chore: updated author, discussions-to and removed various links --- EIPS/eip-5725.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index bdb9ef25b37288..3c57e0581c1a38 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -2,8 +2,8 @@ eip: 5725 title: Transferable Vesting NFT description: An interface for transferable vesting NFTs which release underlying tokens over time. -author: Apeguru (@Apegurus), Marco (CTO & Co-Founder @0xPaladinSec), Mario (Lead of Development Team @0xPaladinSec), DeFiFoFum (@DeFiFoFum) -discussions-to: +author: Apeguru (@Apegurus), Marco De Vries , Mario , DeFiFoFum (@DeFiFoFum) +discussions-to: https://ethereum-magicians.org/t/eip-5725-transferable-vesting-nft/11099 status: Draft type: Standards Track category: ERC @@ -161,12 +161,12 @@ interface IVestingNFT is IERC721 { - _vesting_: Tokens which are locked until a future date - _vested_: Tokens which have reached their unlock date. (The usage in this specification relates to the **total** vested tokens for a given Vesting NFT.) - _claimable_: Amount of tokens which can be claimed at the current `timestamp`. -- _timestamp_: The [unix](https://www.unixtimestamp.com/) `timestamp` (seconds) representation of dates used for vesting. +- _timestamp_: The unix `timestamp` (seconds) representation of dates used for vesting. **vesting functions** - `vestingPayout()` and `vestedPayout()` add up to the total number of tokens which can be claimed by the end of of the vesting schedule, which is also equal to `vestedPayoutAtTime()` with `type(uint256).max` as the `timestamp`. - `vestedPayout()` will provide the total amount of tokens which are eligible for release (including claimed tokens), while `claimablePayout()` provides the amount of tokens which can be claimed at the current `timestamp`. -- `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. It would be incredible to see integrations such as [hot-chain-svg](https://github.com/w1nt3r-eth/hot-chain-svg) to be able to create SVG visuals of vesting curves directly on-chain. +- `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. It would be incredible to see integrations that create SVG visuals of vesting curves directly on-chain. @@ -209,9 +209,6 @@ Standards - [EIP-165](./eip-165.md) Standard Interface Detection. - [EIP-721](./eip-721.md) Token Standard. -- [Timestamp Dependence](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/timestamp-dependence/#the-15-second-rule) The 15-second Rule. -- [hot-chain-svg](https://github.com/w1nt3r-eth/hot-chain-svg) On-chain SVG generator. Could be used to generate vesting curves for Vesting NFTs on-chain. - ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). From 11e64dc88a83897eea7a09d6d6a6d2727e0b8ff7 Mon Sep 17 00:00:00 2001 From: MarcoPaladin Date: Fri, 30 Sep 2022 15:47:29 +0200 Subject: [PATCH 06/22] chore: Moved reference implementation to assets --- EIPS/eip-5725.md | 2 +- assets/eip-5725/contracts/BaseVestingNFT.sol | 156 ++++++++++++++++ assets/eip-5725/contracts/IVestingNFT.sol | 79 ++++++++ assets/eip-5725/contracts/mocks/ERC20Mock.sol | 26 +++ .../contracts/reference/LinearVestingNFT.sol | 119 ++++++++++++ .../contracts/reference/VestingNFT.sol | 99 ++++++++++ assets/eip-5725/test/LinearVestingNFT.test.ts | 123 ++++++++++++ assets/eip-5725/test/VestingNFT.test.ts | 175 ++++++++++++++++++ assets/eip-5725/test/helpers/time.ts | 12 ++ 9 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 assets/eip-5725/contracts/BaseVestingNFT.sol create mode 100644 assets/eip-5725/contracts/IVestingNFT.sol create mode 100644 assets/eip-5725/contracts/mocks/ERC20Mock.sol create mode 100644 assets/eip-5725/contracts/reference/LinearVestingNFT.sol create mode 100644 assets/eip-5725/contracts/reference/VestingNFT.sol create mode 100644 assets/eip-5725/test/LinearVestingNFT.test.ts create mode 100644 assets/eip-5725/test/VestingNFT.test.ts create mode 100644 assets/eip-5725/test/helpers/time.ts diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 3c57e0581c1a38..55162063aa65bd 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -186,7 +186,7 @@ The reference vesting NFT repository includes tests written in Hardhat. ## Reference Implementation -A reference implementation of this EIP can be found in [this repository](https://github.com/ApeSwapFinance/eip-5725-vesting-nft-implementation). +A reference implementation of this EIP can be found in [this repository](../assets/eip-5725/contracts/). ## Security Considerations diff --git a/assets/eip-5725/contracts/BaseVestingNFT.sol b/assets/eip-5725/contracts/BaseVestingNFT.sol new file mode 100644 index 00000000000000..63a7060ce989ee --- /dev/null +++ b/assets/eip-5725/contracts/BaseVestingNFT.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; +import "./IVestingNFT.sol"; + +abstract contract BaseVestingNFT is IVestingNFT, ERC721 { + using SafeERC20 for IERC20; + + /// @dev mapping for claimed payouts + mapping(uint256 => uint256) /*tokenId*/ /*claimed*/ + internal _payoutClaimed; + + /** + * @notice Checks if the tokenId exists and its valid + * @param tokenId The NFT token id + */ + modifier validToken(uint256 tokenId) { + require(_exists(tokenId), "VestingNFT: invalid token ID"); + _; + } + + /** + * @dev See {IVestingNFT}. + */ + function claim(uint256 tokenId) external override(IVestingNFT) validToken(tokenId) returns (uint256 amountClaimed) { + require(ownerOf(tokenId) == msg.sender, "Not owner of NFT"); + amountClaimed = claimablePayout(tokenId); + require(amountClaimed > 0, "VestingNFT: No pending payout"); + + emit PayoutClaimed(tokenId, msg.sender, amountClaimed); + + _payoutClaimed[tokenId] += amountClaimed; + IERC20(payoutToken(tokenId)).safeTransfer(msg.sender, amountClaimed); + } + + /** + * @dev See {IVestingNFT}. + */ + function vestedPayout(uint256 tokenId) public view override(IVestingNFT) returns (uint256 payout) { + return vestedPayoutAtTime(tokenId, block.timestamp); + } + + /** + * @dev See {IVestingNFT}. + */ + function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) + public + view + virtual + override(IVestingNFT) + returns (uint256 payout); + + /** + * @dev See {IVestingNFT}. + */ + function vestingPayout(uint256 tokenId) + public + view + override(IVestingNFT) + validToken(tokenId) + returns (uint256 payout) + { + return _payout(tokenId) - vestedPayout(tokenId); + } + + /** + * @dev See {IVestingNFT}. + */ + function claimablePayout(uint256 tokenId) + public + view + override(IVestingNFT) + validToken(tokenId) + returns (uint256 payout) + { + return vestedPayout(tokenId) - _payoutClaimed[tokenId]; + } + + /** + * @dev See {IVestingNFT}. + */ + function vestingPeriod(uint256 tokenId) + public + view + override(IVestingNFT) + validToken(tokenId) + returns (uint256 vestingStart, uint256 vestingEnd) + { + return (_startTime(tokenId), _endTime(tokenId)); + } + + /** + * @dev See {IVestingNFT}. + */ + function payoutToken(uint256 tokenId) + public + view + override(IVestingNFT) + validToken(tokenId) + returns (address token) + { + return _payoutToken(tokenId); + } + + /** + * @dev See {IERC165-supportsInterface}. + * IVestingNFT interfaceId = 0xf8600f8b + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721, IERC165) + returns (bool supported) + { + return interfaceId == type(IVestingNFT).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev Internal function to get the payout token of a given vesting NFT + * + * @param tokenId on which to check the payout token address + * @return address payout token address + */ + function _payoutToken(uint256 tokenId) internal view virtual returns (address); + + /** + * @dev Internal function to get the total payout of a given vesting NFT. + * @dev This is the total that will be paid out to the NFT owner, including historical tokens. + * + * @param tokenId to check + * @return uint256 the total payout of a given vesting NFT + */ + function _payout(uint256 tokenId) internal view virtual returns (uint256); + + /** + * @dev Internal function to get the start time of a given vesting NFT + * + * @param tokenId to check + * @return uint256 the start time in epoch timestamp + */ + function _startTime(uint256 tokenId) internal view virtual returns (uint256); + + /** + * @dev Internal function to get the end time of a given vesting NFT + * + * @param tokenId to check + * @return uint256 the end time in epoch timestamp + */ + function _endTime(uint256 tokenId) internal view virtual returns (uint256); +} diff --git a/assets/eip-5725/contracts/IVestingNFT.sol b/assets/eip-5725/contracts/IVestingNFT.sol new file mode 100644 index 00000000000000..3d15a566ad5440 --- /dev/null +++ b/assets/eip-5725/contracts/IVestingNFT.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^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 tokens (ERC-20 or otherwise) 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 alloted for a specific Vesting NFT. + */ +interface IVestingNFT is IERC721 { + event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); + + /** + * @notice Claim the pending payout for the NFT + * @dev MUST grant the claimablePayout value at the time of claim being called + * MUST revert if not called by the token owner or approved users + * SHOULD revert if there is nothing to claim + * @param tokenId The NFT token id + * @return amountClaimed The amount of tokens claimed in this call + */ + function claim(uint256 tokenId) external returns (uint256 amountClaimed); + + /** + * @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. + */ + function vestedPayout(uint256 tokenId) external view returns (uint256 payout); + + /** + * @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 + */ + function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout); + + /** + * @notice Number of tokens for an NFT which are currently vesting (locked). + * @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 have not been claimed yet, + * regardless of whether they are ready to claim + */ + function vestingPayout(uint256 tokenId) external view returns (uint256 payout); + + /** + * @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()` value with the total + * amount of tokens claimed subtracted. + * @param tokenId The NFT token id + * @return payout The number of vested tokens for the NFT which have not been claimed yet + */ + function claimablePayout(uint256 tokenId) external view returns (uint256 payout); + + /** + * @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 + */ + function vestingPeriod(uint256 tokenId) external view returns (uint256 vestingStart, uint256 vestingEnd); + + /** + * @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 + */ + function payoutToken(uint256 tokenId) external view returns (address token); +} diff --git a/assets/eip-5725/contracts/mocks/ERC20Mock.sol b/assets/eip-5725/contracts/mocks/ERC20Mock.sol new file mode 100644 index 00000000000000..51639ed07804e8 --- /dev/null +++ b/assets/eip-5725/contracts/mocks/ERC20Mock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ERC20Mock is ERC20 { + uint8 private _decimals; + + constructor( + uint256 supply_, + uint8 decimals_, + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) { + _mint(msg.sender, supply_); + _decimals = decimals_; + } + + function mint(uint256 amount, address to) public { + _mint(to, amount); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} diff --git a/assets/eip-5725/contracts/reference/LinearVestingNFT.sol b/assets/eip-5725/contracts/reference/LinearVestingNFT.sol new file mode 100644 index 00000000000000..5e85ffd0bee112 --- /dev/null +++ b/assets/eip-5725/contracts/reference/LinearVestingNFT.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import "../BaseVestingNFT.sol"; + +contract LinearVestingNFT is BaseVestingNFT { + using SafeERC20 for IERC20; + + struct VestDetails { + IERC20 payoutToken; /// @dev payout token + uint256 payout; /// @dev payout token remaining to be paid + uint128 startTime; /// @dev when vesting starts + uint128 endTime; /// @dev when vesting end + uint128 cliff; /// @dev duration in seconds of the cliff in which tokens will be begin releasing + } + mapping(uint256 => VestDetails) public vestDetails; /// @dev maps the vesting data with tokenIds + + /// @dev tracker of current NFT id + uint256 private _tokenIdTracker; + + /** + * @dev See {IVestingNFT}. + */ + constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + + /** + * @notice Creates a new vesting NFT and mints it + * @dev Token amount should be approved to be transferred by this contract before executing create + * @param to The recipient of the NFT + * @param amount The total assets to be locked over time + * @param startTime When the vesting starts in epoch timestamp + * @param duration The vesting duration in seconds + * @param cliff The cliff duration in seconds + * @param token The ERC20 token to vest over time + */ + function create( + address to, + uint256 amount, + uint128 startTime, + uint128 duration, + uint128 cliff, + IERC20 token + ) public virtual { + require(startTime >= block.timestamp, "startTime cannot be on the past"); + require(to != address(0), "to cannot be address 0"); + require(cliff <= duration, "duration needs to be more than cliff"); + + uint256 newTokenId = _tokenIdTracker; + + vestDetails[newTokenId] = VestDetails({ + payoutToken: token, + payout: amount, + startTime: startTime, + endTime: startTime + duration, + cliff: startTime + cliff + }); + + _tokenIdTracker++; + _mint(to, newTokenId); + IERC20(payoutToken(newTokenId)).safeTransferFrom(msg.sender, address(this), amount); + } + + /** + * @dev See {IVestingNFT}. + */ + function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) + public + view + override(BaseVestingNFT) + validToken(tokenId) + returns (uint256 payout) + { + if (timestamp < _cliff(tokenId)) { + return 0; + } + if (timestamp > _endTime(tokenId)) { + return _payout(tokenId); + } + return (_payout(tokenId) * (timestamp - _startTime(tokenId))) / (_endTime(tokenId) - _startTime(tokenId)); + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _payoutToken(uint256 tokenId) internal view override returns (address) { + return address(vestDetails[tokenId].payoutToken); + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _payout(uint256 tokenId) internal view override returns (uint256) { + return vestDetails[tokenId].payout; + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _startTime(uint256 tokenId) internal view override returns (uint256) { + return vestDetails[tokenId].startTime; + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _endTime(uint256 tokenId) internal view override returns (uint256) { + return vestDetails[tokenId].endTime; + } + + /** + * @dev Internal function to get the cliff time of a given linear vesting NFT + * + * @param tokenId to check + * @return uint256 the cliff time in seconds + */ + function _cliff(uint256 tokenId) internal view returns (uint256) { + return vestDetails[tokenId].cliff; + } +} diff --git a/assets/eip-5725/contracts/reference/VestingNFT.sol b/assets/eip-5725/contracts/reference/VestingNFT.sol new file mode 100644 index 00000000000000..f6317c9d2d2328 --- /dev/null +++ b/assets/eip-5725/contracts/reference/VestingNFT.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.17; + +import "../BaseVestingNFT.sol"; + +contract VestingNFT is BaseVestingNFT { + using SafeERC20 for IERC20; + + struct VestDetails { + IERC20 payoutToken; /// @dev payout token + uint256 payout; /// @dev payout token remaining to be paid + uint128 startTime; /// @dev when vesting starts + uint128 endTime; /// @dev when vesting end + } + mapping(uint256 => VestDetails) public vestDetails; /// @dev maps the vesting data with tokenIds + + /// @dev tracker of current NFT id + uint256 private _tokenIdTracker; + + /** + * @dev Initializes the contract by setting a `name` and a `symbol` to the token. + */ + constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + + /** + * @notice Creates a new vesting NFT and mints it + * @dev Token amount should be approved to be transferred by this contract before executing create + * @param to The recipient of the NFT + * @param amount The total assets to be locked over time + * @param releaseTimestamp When the full amount of tokens get released + * @param token The ERC20 token to vest over time + */ + function create( + address to, + uint256 amount, + uint128 releaseTimestamp, + IERC20 token + ) public virtual { + require(to != address(0), "to cannot be address 0"); + require(releaseTimestamp > block.timestamp, "release must be in future"); + + uint256 newTokenId = _tokenIdTracker; + + vestDetails[newTokenId] = VestDetails({ + payoutToken: token, + payout: amount, + startTime: uint128(block.timestamp), + endTime: releaseTimestamp + }); + + _tokenIdTracker++; + _mint(to, newTokenId); + IERC20(payoutToken(newTokenId)).safeTransferFrom(msg.sender, address(this), amount); + } + + /** + * @dev See {IVestingNFT}. + */ + function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) + public + view + override(BaseVestingNFT) + validToken(tokenId) + returns (uint256 payout) + { + if (timestamp >= _endTime(tokenId)) { + return _payout(tokenId); + } + return 0; + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _payoutToken(uint256 tokenId) internal view override returns (address) { + return address(vestDetails[tokenId].payoutToken); + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _payout(uint256 tokenId) internal view override returns (uint256) { + return vestDetails[tokenId].payout; + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _startTime(uint256 tokenId) internal view override returns (uint256) { + return vestDetails[tokenId].startTime; + } + + /** + * @dev See {BaseVestingNFT}. + */ + function _endTime(uint256 tokenId) internal view override returns (uint256) { + return vestDetails[tokenId].endTime; + } +} diff --git a/assets/eip-5725/test/LinearVestingNFT.test.ts b/assets/eip-5725/test/LinearVestingNFT.test.ts new file mode 100644 index 00000000000000..7d1b23cc04fa21 --- /dev/null +++ b/assets/eip-5725/test/LinearVestingNFT.test.ts @@ -0,0 +1,123 @@ +import { ethers } from 'hardhat' +import { Signer } from 'ethers' +import { expect } from 'chai' +import { increaseTime } from './helpers/time' +// typechain +import { + ERC20Mock__factory, + ERC20Mock, + LinearVestingNFT__factory, + LinearVestingNFT, +} from '../typechain-types' + +const testValues = { + payout: '1000000000', + lockTime: 60, + buffer: 10, + totalLock: 70, +} + +describe('LinearVestingNFT', function () { + let accounts: Signer[] + let linearVestingNFT: LinearVestingNFT + let mockToken: ERC20Mock + let receiverAccount: string + let unlockTime: number + + beforeEach(async function () { + const LinearVestingNFT = (await ethers.getContractFactory( + 'LinearVestingNFT' + )) as LinearVestingNFT__factory + linearVestingNFT = await LinearVestingNFT.deploy('LinearVestingNFT', 'TLV') + await linearVestingNFT.deployed() + + const ERC20Mock = (await ethers.getContractFactory( + 'ERC20Mock' + )) as ERC20Mock__factory + mockToken = await ERC20Mock.deploy( + '1000000000000000000000', + 18, + 'LockedToken', + 'LOCK' + ) + await mockToken.deployed() + await mockToken.approve(linearVestingNFT.address, '1000000000000000000000') + + accounts = await ethers.getSigners() + receiverAccount = await accounts[1].getAddress() + unlockTime = await createVestingNft( + linearVestingNFT, + receiverAccount, + mockToken + ) + }) + + it('Returns a valid vested payout', async function () { + // TODO: More extensive testing of linear vesting functionality + const totalPayout = await linearVestingNFT.vestedPayoutAtTime(0, unlockTime) + expect(await linearVestingNFT.vestedPayout(0)).to.equal(0) + await increaseTime(testValues.totalLock) + expect(await linearVestingNFT.vestedPayout(0)).to.equal(totalPayout) + }) + + it('Reverts when creating to account 0', async function () { + const latestBlock = await ethers.provider.getBlock('latest') + await expect( + linearVestingNFT.create( + '0x0000000000000000000000000000000000000000', + testValues.payout, + latestBlock.timestamp + testValues.buffer, + testValues.lockTime, + 0, + mockToken.address + ) + ).to.revertedWith('to cannot be address 0') + }) + + it('Reverts when creating to past start date 0', async function () { + await expect( + linearVestingNFT.create( + receiverAccount, + testValues.payout, + 0, + testValues.lockTime, + 0, + mockToken.address + ) + ).to.revertedWith('startTime cannot be on the past') + }) + + it('Reverts when duration is less than cliff', async function () { + const latestBlock = await ethers.provider.getBlock('latest') + await expect( + linearVestingNFT.create( + receiverAccount, + testValues.payout, + latestBlock.timestamp + testValues.buffer, + testValues.lockTime, + 100, + mockToken.address + ) + ).to.revertedWith('duration needs to be more than cliff') + }) +}) + +async function createVestingNft( + linearVestingNFT: LinearVestingNFT, + receiverAccount: string, + mockToken: ERC20Mock +) { + const latestBlock = await ethers.provider.getBlock('latest') + const unlockTime = + latestBlock.timestamp + testValues.lockTime + testValues.buffer + const txReceipt = await linearVestingNFT.create( + receiverAccount, + testValues.payout, + latestBlock.timestamp + testValues.buffer, + testValues.lockTime, + 0, + mockToken.address + ) + await txReceipt.wait() + return unlockTime +} diff --git a/assets/eip-5725/test/VestingNFT.test.ts b/assets/eip-5725/test/VestingNFT.test.ts new file mode 100644 index 00000000000000..e452544b0764b6 --- /dev/null +++ b/assets/eip-5725/test/VestingNFT.test.ts @@ -0,0 +1,175 @@ +import { ethers } from 'hardhat' +import { Signer } from 'ethers' +import { expect } from 'chai' +import { increaseTime } from './helpers/time' +// typechain +import { ERC20Mock, VestingNFT } from '../typechain-types' + +const testValues = { + payout: '1000000000', + lockTime: 60, +} + +describe('VestingNFT', function () { + let accounts: Signer[] + let vestingNFT: VestingNFT + let mockToken: ERC20Mock + let receiverAccount: string + let unlockTime: number + + beforeEach(async function () { + const VestingNFT = await ethers.getContractFactory('VestingNFT') + vestingNFT = await VestingNFT.deploy('VestingNFT', 'TLV') + await vestingNFT.deployed() + + const ERC20Mock = await ethers.getContractFactory('ERC20Mock') + mockToken = await ERC20Mock.deploy( + '1000000000000000000000', + 18, + 'LockedToken', + 'LOCK' + ) + await mockToken.deployed() + await mockToken.approve(vestingNFT.address, '1000000000000000000000') + + accounts = await ethers.getSigners() + receiverAccount = await accounts[1].getAddress() + unlockTime = await createVestingNft(vestingNFT, receiverAccount, mockToken) + }) + + it('Supports ERC721 and IVestingNFT interfaces', async function () { + expect(await vestingNFT.supportsInterface('0x80ac58cd')).to.equal(true) + + /** + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified + * // Solidity export interface id: + * bytes4 public constant IID_ITEST = type(IVestingNFT).interfaceId; + * // Pull out the interfaceId in tests + * const interfaceId = await vestingNFT.IID_ITEST(); + */ + // Vesting NFT Interface ID + expect(await vestingNFT.supportsInterface('0xf8600f8b')).to.equal(true) + }) + + it('Returns a valid vested payout', async function () { + const totalPayout = await vestingNFT.vestedPayoutAtTime(0, unlockTime) + expect(await vestingNFT.vestedPayout(0)).to.equal(0) + await increaseTime(testValues.lockTime) + expect(await vestingNFT.vestedPayout(0)).to.equal(totalPayout) + }) + + it('Reverts with invalid ID', async function () { + await expect(vestingNFT.vestedPayout(1)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + await expect(vestingNFT.vestedPayoutAtTime(1, unlockTime)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + await expect(vestingNFT.vestingPayout(1)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + await expect(vestingNFT.claimablePayout(1)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + await expect(vestingNFT.vestingPeriod(1)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + await expect(vestingNFT.payoutToken(1)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + await expect(vestingNFT.claim(1)).to.revertedWith( + 'VestingNFT: invalid token ID' + ) + // NOTE: Removed claimTo from spec + // await expect(vestingNFT.claimTo(1, receiverAccount)).to.revertedWith( + // "VestingNFT: invalid token ID" + // ); + }) + + it('Returns a valid pending payout', async function () { + expect(await vestingNFT.vestingPayout(0)).to.equal(testValues.payout) + }) + + it('Returns a valid releasable payout', async function () { + const totalPayout = await vestingNFT.vestedPayoutAtTime(0, unlockTime) + expect(await vestingNFT.claimablePayout(0)).to.equal(0) + await increaseTime(testValues.lockTime) + expect(await vestingNFT.claimablePayout(0)).to.equal(totalPayout) + }) + + it('Returns a valid vesting period', async function () { + const vestingPeriod = await vestingNFT.vestingPeriod(0) + expect(vestingPeriod.vestingEnd).to.equal(unlockTime) + }) + + it('Returns a valid payout token', async function () { + expect(await vestingNFT.payoutToken(0)).to.equal(mockToken.address) + }) + + it('Is able to claim', async function () { + const connectedVestingNft = vestingNFT.connect(accounts[1]) + await increaseTime(testValues.lockTime) + const txReceipt = await connectedVestingNft.claim(0) + await txReceipt.wait() + expect(await mockToken.balanceOf(receiverAccount)).to.equal( + testValues.payout + ) + }) + + it('Reverts claim when payout is 0', async function () { + const connectedVestingNft = vestingNFT.connect(accounts[1]) + await expect(connectedVestingNft.claim(0)).to.revertedWith( + 'VestingNFT: No pending payout' + ) + }) + + it('Reverts claim when payout is not from owner', async function () { + const connectedVestingNft = vestingNFT.connect(accounts[2]) + await expect(connectedVestingNft.claim(0)).to.revertedWith( + 'Not owner of NFT' + ) + }) + + // NOTE: Removed claimTo from spec + // it("Is able to claim to other account", async function () { + // const connectedVestingNft = vestingNFT.connect(accounts[1]); + // const otherReceiverAddress = await accounts[2].getAddress(); + // await increaseTime(testValues.lockTime); + // const txReceipt = await connectedVestingNft.claimTo( + // 0, + // otherReceiverAddress + // ); + // await txReceipt.wait(); + // expect(await mockToken.balanceOf(otherReceiverAddress)).to.equal( + // testValues.payout + // ); + // }); + + it('Reverts when creating to account 0', async function () { + await expect( + vestingNFT.create( + '0x0000000000000000000000000000000000000000', + testValues.payout, + unlockTime, + mockToken.address + ) + ).to.revertedWith('to cannot be address 0') + }) +}) + +async function createVestingNft( + vestingNFT: VestingNFT, + receiverAccount: string, + mockToken: ERC20Mock +) { + const latestBlock = await ethers.provider.getBlock('latest') + const unlockTime = latestBlock.timestamp + testValues.lockTime + const txReceipt = await vestingNFT.create( + receiverAccount, + testValues.payout, + unlockTime, + mockToken.address + ) + await txReceipt.wait() + return unlockTime +} diff --git a/assets/eip-5725/test/helpers/time.ts b/assets/eip-5725/test/helpers/time.ts new file mode 100644 index 00000000000000..6e99622c19a765 --- /dev/null +++ b/assets/eip-5725/test/helpers/time.ts @@ -0,0 +1,12 @@ +import { ethers } from 'hardhat' + +async function mineNBlocks(n: number) { + for (let index = 0; index < n; index++) { + await ethers.provider.send('evm_mine', []) + } +} + +export async function increaseTime(seconds: number) { + await ethers.provider.send('evm_increaseTime', [seconds]) + await mineNBlocks(1) +} From 30062713ba981682f98cc49b466c5f5ffc0e5728 Mon Sep 17 00:00:00 2001 From: MarcoPaladin Date: Fri, 30 Sep 2022 16:01:55 +0200 Subject: [PATCH 07/22] chore: removed invalid EIP sections --- EIPS/eip-5725.md | 48 ++++++++++-------------------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 55162063aa65bd..1d00f1832ae0e0 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -13,29 +13,9 @@ requires: 721 # [EIP-5725](./eip-5725.md) Vesting NFT -## Table of Contents -- [EIP-5725 Vesting NFT](#eip-5725-vesting-nft) - - [Table of Contents](#table-of-contents) - - [Simple Summary](#simple-summary) - - [Abstract](#abstract) - - [Motivation](#motivation) - - [Use Cases](#use-cases) - - [Specification](#specification) - - [Rationale](#rationale) - - [Backwards Compatibility](#backwards-compatibility) - - [Reference Implementation](#reference-implementation) - - [Test Cases](#test-cases) - - [Security Considerations](#security-considerations) - - [Extensions](#extensions) - - [References](#references) - - [Copyright](#copyright) - - [Citation](#citation) - - -## Simple Summary +## Abstract A **Non-Fungible Token** (NFT) standard used to vest tokens ([EIP-20](./eip-20.md) or otherwise) over a vesting release curve. -## Abstract 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 ([EIP-20](./eip-20.md) or otherwise) that is emitted to the NFT holder. This standard is an extension of the [EIP-721](./eip-721.md) token that provides basic functionality for creating vesting NFTs, claiming the tokens and reading vesting curve properties. ## Motivation @@ -175,6 +155,14 @@ Generally in Solidity development it is advised against using `block.timestamp` 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** + +The standard does not implement the following features: +- Vesting Curves +- Rental +- Beneficiary + +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 @@ -197,21 +185,5 @@ A reference implementation of this EIP can be found in [this repository](../asse **approvals** - When an 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. -## Extensions -- Vesting Curves -- Rental -- Beneficiary - -## References -Standards - -- [EIP-20](./eip-20.md) Token Standard. -- [EIP-165](./eip-165.md) Standard Interface Detection. -- [EIP-721](./eip-721.md) Token Standard. - ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). - -## Citation -Please cite this document as: -Apeguru(@Apegurus), Marco, Mario, DeFiFoFum, "EIP-5725: Vesting NFT," Ethereum Improvement Proposals, no. XXXX, September 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-XXXX. \ No newline at end of file +Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file From 068ababf939146e8d8a7c25ed28d0758749e1994 Mon Sep 17 00:00:00 2001 From: DeFiFoFum <78645267+DeFiFoFum@users.noreply.github.com> Date: Tue, 4 Oct 2022 13:23:07 -0500 Subject: [PATCH 08/22] refactor: Remove comments from EIP-5725 Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5725.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 1d00f1832ae0e0..f35e569d6dcb60 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -49,7 +49,6 @@ These curves could represent: ## 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. ```solidity @@ -136,7 +135,6 @@ interface IVestingNFT is IERC721 { ## Rationale - **vesting terms** - _vesting_: Tokens which are locked until a future date - _vested_: Tokens which have reached their unlock date. (The usage in this specification relates to the **total** vested tokens for a given Vesting NFT.) @@ -165,7 +163,6 @@ The standard does not implement the following features: 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 [EIP-721 standard](./eip-721.md) integrations and marketplaces. - The Vesting NFT standard also supports [EIP-165 standard](./eip-165.md) interface detection for detecting `EIP-721` compatibility, as well as Vesting NFT compatibility. @@ -173,11 +170,9 @@ This is done intentionally to keep the base standard simple. These features can The reference vesting NFT repository includes tests written in Hardhat. ## Reference Implementation - A reference implementation of this EIP can be found in [this repository](../assets/eip-5725/contracts/). ## 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` From 3a8ee84a510e8893037cbffb1df180e8e8c4201c Mon Sep 17 00:00:00 2001 From: defifofum Date: Tue, 4 Oct 2022 13:35:58 -0500 Subject: [PATCH 09/22] refactor: Update SPDX license to CC0-1.0 --- EIPS/eip-5725.md | 2 +- assets/eip-5725/contracts/BaseVestingNFT.sol | 2 +- assets/eip-5725/contracts/IVestingNFT.sol | 2 +- assets/eip-5725/contracts/mocks/ERC20Mock.sol | 2 +- assets/eip-5725/contracts/reference/LinearVestingNFT.sol | 2 +- assets/eip-5725/contracts/reference/VestingNFT.sol | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index f35e569d6dcb60..8e51d4eee0dcd2 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -52,7 +52,7 @@ These curves could represent: 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. ```solidity -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; diff --git a/assets/eip-5725/contracts/BaseVestingNFT.sol b/assets/eip-5725/contracts/BaseVestingNFT.sol index 63a7060ce989ee..dcdbac4430b356 100644 --- a/assets/eip-5725/contracts/BaseVestingNFT.sol +++ b/assets/eip-5725/contracts/BaseVestingNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.17; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/assets/eip-5725/contracts/IVestingNFT.sol b/assets/eip-5725/contracts/IVestingNFT.sol index 3d15a566ad5440..21dace6171c078 100644 --- a/assets/eip-5725/contracts/IVestingNFT.sol +++ b/assets/eip-5725/contracts/IVestingNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; diff --git a/assets/eip-5725/contracts/mocks/ERC20Mock.sol b/assets/eip-5725/contracts/mocks/ERC20Mock.sol index 51639ed07804e8..c006a24ea0f7c7 100644 --- a/assets/eip-5725/contracts/mocks/ERC20Mock.sol +++ b/assets/eip-5725/contracts/mocks/ERC20Mock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity 0.8.17; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/assets/eip-5725/contracts/reference/LinearVestingNFT.sol b/assets/eip-5725/contracts/reference/LinearVestingNFT.sol index 5e85ffd0bee112..ec2bc8ea82b554 100644 --- a/assets/eip-5725/contracts/reference/LinearVestingNFT.sol +++ b/assets/eip-5725/contracts/reference/LinearVestingNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.17; import "../BaseVestingNFT.sol"; diff --git a/assets/eip-5725/contracts/reference/VestingNFT.sol b/assets/eip-5725/contracts/reference/VestingNFT.sol index f6317c9d2d2328..91a65d0157d52c 100644 --- a/assets/eip-5725/contracts/reference/VestingNFT.sol +++ b/assets/eip-5725/contracts/reference/VestingNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.17; import "../BaseVestingNFT.sol"; From 761abc6327f3cf22193fdf70db64312fe2214571 Mon Sep 17 00:00:00 2001 From: defifofum Date: Tue, 4 Oct 2022 13:43:50 -0500 Subject: [PATCH 10/22] refactor: Rename IVestingNFT to IERC5725 --- EIPS/eip-5725.md | 4 +-- .../{BaseVestingNFT.sol => ERC5725.sol} | 36 +++++++++---------- .../{IVestingNFT.sol => IERC5725.sol} | 2 +- .../contracts/reference/LinearVestingNFT.sol | 18 +++++----- .../contracts/reference/VestingNFT.sol | 16 ++++----- assets/eip-5725/test/VestingNFT.test.ts | 4 +-- 6 files changed, 40 insertions(+), 40 deletions(-) rename assets/eip-5725/contracts/{BaseVestingNFT.sol => ERC5725.sol} (83%) rename assets/eip-5725/contracts/{IVestingNFT.sol => IERC5725.sol} (99%) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 8e51d4eee0dcd2..b5e9b146d7f868 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -44,7 +44,7 @@ These curves could represent: 6. Enable standardized fundraising implementations and general fundraising that sell vesting tokens (eg. SAFTs) in a more transparent manner. 7. 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 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 [EIP-165](./eip-165.md) supportsInterface(IVestingNFT) check. + - Such tooling can easily discover compliance through the [EIP-165](./eip-165.md) supportsInterface(IERC5725) check. 8. 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. @@ -63,7 +63,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * @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 alloted for a specific Vesting NFT. */ -interface IVestingNFT is IERC721 { +interface IERC5725 is IERC721 { event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); /** diff --git a/assets/eip-5725/contracts/BaseVestingNFT.sol b/assets/eip-5725/contracts/ERC5725.sol similarity index 83% rename from assets/eip-5725/contracts/BaseVestingNFT.sol rename to assets/eip-5725/contracts/ERC5725.sol index dcdbac4430b356..a840ae4b4fb26a 100644 --- a/assets/eip-5725/contracts/BaseVestingNFT.sol +++ b/assets/eip-5725/contracts/ERC5725.sol @@ -6,9 +6,9 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; -import "./IVestingNFT.sol"; +import "./IERC5725.sol"; -abstract contract BaseVestingNFT is IVestingNFT, ERC721 { +abstract contract ERC5725 is IERC5725, ERC721 { using SafeERC20 for IERC20; /// @dev mapping for claimed payouts @@ -25,9 +25,9 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ - function claim(uint256 tokenId) external override(IVestingNFT) validToken(tokenId) returns (uint256 amountClaimed) { + function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) returns (uint256 amountClaimed) { require(ownerOf(tokenId) == msg.sender, "Not owner of NFT"); amountClaimed = claimablePayout(tokenId); require(amountClaimed > 0, "VestingNFT: No pending payout"); @@ -39,29 +39,29 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ - function vestedPayout(uint256 tokenId) public view override(IVestingNFT) returns (uint256 payout) { + function vestedPayout(uint256 tokenId) public view override(IERC5725) returns (uint256 payout) { return vestedPayoutAtTime(tokenId, block.timestamp); } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) public view virtual - override(IVestingNFT) + override(IERC5725) returns (uint256 payout); /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function vestingPayout(uint256 tokenId) public view - override(IVestingNFT) + override(IERC5725) validToken(tokenId) returns (uint256 payout) { @@ -69,12 +69,12 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function claimablePayout(uint256 tokenId) public view - override(IVestingNFT) + override(IERC5725) validToken(tokenId) returns (uint256 payout) { @@ -82,12 +82,12 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function vestingPeriod(uint256 tokenId) public view - override(IVestingNFT) + override(IERC5725) validToken(tokenId) returns (uint256 vestingStart, uint256 vestingEnd) { @@ -95,12 +95,12 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function payoutToken(uint256 tokenId) public view - override(IVestingNFT) + override(IERC5725) validToken(tokenId) returns (address token) { @@ -109,7 +109,7 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { /** * @dev See {IERC165-supportsInterface}. - * IVestingNFT interfaceId = 0xf8600f8b + * IERC5725 interfaceId = 0xf8600f8b */ function supportsInterface(bytes4 interfaceId) public @@ -118,7 +118,7 @@ abstract contract BaseVestingNFT is IVestingNFT, ERC721 { override(ERC721, IERC165) returns (bool supported) { - return interfaceId == type(IVestingNFT).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IERC5725).interfaceId || super.supportsInterface(interfaceId); } /** diff --git a/assets/eip-5725/contracts/IVestingNFT.sol b/assets/eip-5725/contracts/IERC5725.sol similarity index 99% rename from assets/eip-5725/contracts/IVestingNFT.sol rename to assets/eip-5725/contracts/IERC5725.sol index 21dace6171c078..caaeeaea95aacc 100644 --- a/assets/eip-5725/contracts/IVestingNFT.sol +++ b/assets/eip-5725/contracts/IERC5725.sol @@ -9,7 +9,7 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * @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 alloted for a specific Vesting NFT. */ -interface IVestingNFT is IERC721 { +interface IERC5725 is IERC721 { event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); /** diff --git a/assets/eip-5725/contracts/reference/LinearVestingNFT.sol b/assets/eip-5725/contracts/reference/LinearVestingNFT.sol index ec2bc8ea82b554..a87fd9e062145e 100644 --- a/assets/eip-5725/contracts/reference/LinearVestingNFT.sol +++ b/assets/eip-5725/contracts/reference/LinearVestingNFT.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.17; -import "../BaseVestingNFT.sol"; +import "../ERC5725.sol"; -contract LinearVestingNFT is BaseVestingNFT { +contract LinearVestingNFT is ERC5725 { using SafeERC20 for IERC20; struct VestDetails { @@ -19,7 +19,7 @@ contract LinearVestingNFT is BaseVestingNFT { uint256 private _tokenIdTracker; /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ constructor(string memory name, string memory symbol) ERC721(name, symbol) {} @@ -61,12 +61,12 @@ contract LinearVestingNFT is BaseVestingNFT { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) public view - override(BaseVestingNFT) + override(ERC5725) validToken(tokenId) returns (uint256 payout) { @@ -80,28 +80,28 @@ contract LinearVestingNFT is BaseVestingNFT { } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _payoutToken(uint256 tokenId) internal view override returns (address) { return address(vestDetails[tokenId].payoutToken); } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _payout(uint256 tokenId) internal view override returns (uint256) { return vestDetails[tokenId].payout; } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _startTime(uint256 tokenId) internal view override returns (uint256) { return vestDetails[tokenId].startTime; } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _endTime(uint256 tokenId) internal view override returns (uint256) { return vestDetails[tokenId].endTime; diff --git a/assets/eip-5725/contracts/reference/VestingNFT.sol b/assets/eip-5725/contracts/reference/VestingNFT.sol index 91a65d0157d52c..1b50d85552bc98 100644 --- a/assets/eip-5725/contracts/reference/VestingNFT.sol +++ b/assets/eip-5725/contracts/reference/VestingNFT.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.17; -import "../BaseVestingNFT.sol"; +import "../ERC5725.sol"; -contract VestingNFT is BaseVestingNFT { +contract VestingNFT is ERC5725 { using SafeERC20 for IERC20; struct VestDetails { @@ -54,12 +54,12 @@ contract VestingNFT is BaseVestingNFT { } /** - * @dev See {IVestingNFT}. + * @dev See {IERC5725}. */ function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) public view - override(BaseVestingNFT) + override(ERC5725) validToken(tokenId) returns (uint256 payout) { @@ -70,28 +70,28 @@ contract VestingNFT is BaseVestingNFT { } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _payoutToken(uint256 tokenId) internal view override returns (address) { return address(vestDetails[tokenId].payoutToken); } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _payout(uint256 tokenId) internal view override returns (uint256) { return vestDetails[tokenId].payout; } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _startTime(uint256 tokenId) internal view override returns (uint256) { return vestDetails[tokenId].startTime; } /** - * @dev See {BaseVestingNFT}. + * @dev See {ERC5725}. */ function _endTime(uint256 tokenId) internal view override returns (uint256) { return vestDetails[tokenId].endTime; diff --git a/assets/eip-5725/test/VestingNFT.test.ts b/assets/eip-5725/test/VestingNFT.test.ts index e452544b0764b6..cc2893e77e16f2 100644 --- a/assets/eip-5725/test/VestingNFT.test.ts +++ b/assets/eip-5725/test/VestingNFT.test.ts @@ -37,13 +37,13 @@ describe('VestingNFT', function () { unlockTime = await createVestingNft(vestingNFT, receiverAccount, mockToken) }) - it('Supports ERC721 and IVestingNFT interfaces', async function () { + it('Supports ERC721 and IERC5725 interfaces', async function () { expect(await vestingNFT.supportsInterface('0x80ac58cd')).to.equal(true) /** * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified * // Solidity export interface id: - * bytes4 public constant IID_ITEST = type(IVestingNFT).interfaceId; + * bytes4 public constant IID_ITEST = type(IERC5725).interfaceId; * // Pull out the interfaceId in tests * const interfaceId = await vestingNFT.IID_ITEST(); */ From f15a54113236ba99c7ec9c3cde6ce3ffc54dca84 Mon Sep 17 00:00:00 2001 From: defifofum Date: Tue, 4 Oct 2022 13:57:11 -0500 Subject: [PATCH 11/22] feat: Add EIP-5725 assets README --- EIPS/eip-5725.md | 2 +- assets/eip-5725/README.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 assets/eip-5725/README.md diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 8e51d4eee0dcd2..857b1037724555 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -170,7 +170,7 @@ This is done intentionally to keep the base standard simple. These features can The reference vesting NFT repository includes tests written in Hardhat. ## Reference Implementation -A reference implementation of this EIP can be found in [this repository](../assets/eip-5725/contracts/). +A reference implementation of this EIP can be found in [eip-5725 assets](../assets/eip-5725/README.md/). ## Security Considerations **timestamps** diff --git a/assets/eip-5725/README.md b/assets/eip-5725/README.md new file mode 100644 index 00000000000000..0fd2d94d82c9e5 --- /dev/null +++ b/assets/eip-5725/README.md @@ -0,0 +1,8 @@ +# EIP-5725: Transferrable Vesting NFT - Reference Implementation +This repository serves as a reference implementation for **EIP-5725 Transferrable Vesting NFT Standard**. A Non-Fungible Token (NFT) standard used to vest tokens (ERC-20 or otherwise) over a vesting release curve. + +## Contents +- [EIP-5725 Specification](./contracts/IVestingNFT.sol): Interface and definitions for the EIP-5725 specification. +- [ERC-5725 Implementation (abstract)](./contracts/BaseVestingNFT.sol): ERC-5725 contract which can be extended to implement the specification. +- [VestingNFT Implementation](./contracts/reference/LinearVestingNFT.sol): Full ERC-5725 implementation using cliff vesting curve. +- [LinearVestingNFT Implementation](./contracts/reference/VestingNFT.sol): Full ERC-5725 implementation using linear vesting curve. \ No newline at end of file From 1118b8d1dc9df2c2a27e734972aaab19ecc30a82 Mon Sep 17 00:00:00 2001 From: defifofum Date: Tue, 4 Oct 2022 13:58:50 -0500 Subject: [PATCH 12/22] fix: Links --- assets/eip-5725/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/eip-5725/README.md b/assets/eip-5725/README.md index 0fd2d94d82c9e5..65bfb822051af3 100644 --- a/assets/eip-5725/README.md +++ b/assets/eip-5725/README.md @@ -2,7 +2,7 @@ This repository serves as a reference implementation for **EIP-5725 Transferrable Vesting NFT Standard**. A Non-Fungible Token (NFT) standard used to vest tokens (ERC-20 or otherwise) over a vesting release curve. ## Contents -- [EIP-5725 Specification](./contracts/IVestingNFT.sol): Interface and definitions for the EIP-5725 specification. -- [ERC-5725 Implementation (abstract)](./contracts/BaseVestingNFT.sol): ERC-5725 contract which can be extended to implement the specification. +- [EIP-5725 Specification](./contracts/IERC5725.sol): Interface and definitions for the EIP-5725 specification. +- [ERC-5725 Implementation (abstract)](./contracts/ERC5725.sol): ERC-5725 contract which can be extended to implement the specification. - [VestingNFT Implementation](./contracts/reference/LinearVestingNFT.sol): Full ERC-5725 implementation using cliff vesting curve. - [LinearVestingNFT Implementation](./contracts/reference/VestingNFT.sol): Full ERC-5725 implementation using linear vesting curve. \ No newline at end of file From f89cd06edb7cbe6ca7dbff505f737a564c5d56d0 Mon Sep 17 00:00:00 2001 From: defifofum Date: Tue, 4 Oct 2022 14:00:24 -0500 Subject: [PATCH 13/22] fix: Remove EIP-5725 header --- EIPS/eip-5725.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 857b1037724555..6c194d7a6052ae 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -11,8 +11,6 @@ created: 2022-09-08 requires: 721 --- -# [EIP-5725](./eip-5725.md) Vesting NFT - ## Abstract A **Non-Fungible Token** (NFT) standard used to vest tokens ([EIP-20](./eip-20.md) or otherwise) over a vesting release curve. From 3781d992ada435c766c1be6cd04db954345301cd Mon Sep 17 00:00:00 2001 From: Apeguru <78636484+Apegurus@users.noreply.github.com> Date: Wed, 5 Oct 2022 17:47:11 +0800 Subject: [PATCH 14/22] Apply suggestions from code review Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5725.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 6c194d7a6052ae..6b13650d4f71d0 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -142,7 +142,7 @@ interface IVestingNFT is IERC721 { **vesting functions** - `vestingPayout()` and `vestedPayout()` add up to the total number of tokens which can be claimed by the end of of the vesting schedule, which is also equal to `vestedPayoutAtTime()` with `type(uint256).max` as the `timestamp`. - `vestedPayout()` will provide the total amount of tokens which are eligible for release (including claimed tokens), while `claimablePayout()` provides the amount of tokens which can be claimed at the current `timestamp`. -- `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. It would be incredible to see integrations that create SVG visuals of vesting curves directly on-chain. +- `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. @@ -161,8 +161,8 @@ The standard does not implement the following features: 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 [EIP-721 standard](./eip-721.md) integrations and marketplaces. -- The Vesting NFT standard also supports [EIP-165 standard](./eip-165.md) interface detection for detecting `EIP-721` compatibility, as well as Vesting NFT compatibility. +- The Vesting NFT standard is meant to be fully backwards compatible with any current [EIP-721](./eip-721.md) integrations and marketplaces. +- The Vesting NFT standard also supports [EIP-165](./eip-165.md) 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. From f84d07c9b16c7839cfde835591c9165552cefbcf Mon Sep 17 00:00:00 2001 From: defifofum Date: Fri, 7 Oct 2022 08:35:15 -0500 Subject: [PATCH 15/22] refactor: Remove return value from claim() --- EIPS/eip-5725.md | 3 +-- assets/eip-5725/contracts/ERC5725.sol | 4 ++-- assets/eip-5725/contracts/IERC5725.sol | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 8525daaedf09d0..1b880a5f425a29 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -70,9 +70,8 @@ interface IERC5725 is IERC721 { * MUST revert if not called by the token owner or approved users * SHOULD revert if there is nothing to claim * @param tokenId The NFT token id - * @return amountClaimed The amount of tokens claimed in this call */ - function claim(uint256 tokenId) external returns (uint256 amountClaimed); + function claim(uint256 tokenId) external; /** * @notice Total amount of tokens which have been vested at the current timestamp. diff --git a/assets/eip-5725/contracts/ERC5725.sol b/assets/eip-5725/contracts/ERC5725.sol index a840ae4b4fb26a..dbdc26d62fb7ba 100644 --- a/assets/eip-5725/contracts/ERC5725.sol +++ b/assets/eip-5725/contracts/ERC5725.sol @@ -27,9 +27,9 @@ abstract contract ERC5725 is IERC5725, ERC721 { /** * @dev See {IERC5725}. */ - function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) returns (uint256 amountClaimed) { + function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) { require(ownerOf(tokenId) == msg.sender, "Not owner of NFT"); - amountClaimed = claimablePayout(tokenId); + uint256 amountClaimed = claimablePayout(tokenId); require(amountClaimed > 0, "VestingNFT: No pending payout"); emit PayoutClaimed(tokenId, msg.sender, amountClaimed); diff --git a/assets/eip-5725/contracts/IERC5725.sol b/assets/eip-5725/contracts/IERC5725.sol index caaeeaea95aacc..c4b3327cd3b0d2 100644 --- a/assets/eip-5725/contracts/IERC5725.sol +++ b/assets/eip-5725/contracts/IERC5725.sol @@ -18,9 +18,8 @@ interface IERC5725 is IERC721 { * MUST revert if not called by the token owner or approved users * SHOULD revert if there is nothing to claim * @param tokenId The NFT token id - * @return amountClaimed The amount of tokens claimed in this call */ - function claim(uint256 tokenId) external returns (uint256 amountClaimed); + function claim(uint256 tokenId) external; /** * @notice Total amount of tokens which have been vested at the current timestamp. From 6b60f17ef310bccf5893f012a95b29f9e735f436 Mon Sep 17 00:00:00 2001 From: defifofum Date: Sun, 16 Oct 2022 22:08:39 -0500 Subject: [PATCH 16/22] fix: EIP-N reference --- EIPS/eip-5725.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 1b880a5f425a29..7ace7ae7f8c562 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -42,7 +42,7 @@ These curves could represent: 6. Enable standardized fundraising implementations and general fundraising that sell vesting tokens (eg. SAFTs) in a more transparent manner. 7. 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 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 [EIP-165](./eip-165.md) supportsInterface(IERC5725) check. + - Such tooling can easily discover compliance through the [EIP-165](./eip-165.md) `supportsInterface(InterfaceID)` check. 8. 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. From a49139357aeb0fefc3218bc3054b8742e5f40c43 Mon Sep 17 00:00:00 2001 From: Apeguru <78636484+Apegurus@users.noreply.github.com> Date: Wed, 16 Nov 2022 12:30:42 -0300 Subject: [PATCH 17/22] Update EIPS/eip-5725.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5725.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 7ace7ae7f8c562..26e4e9a25bb5fd 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -17,7 +17,7 @@ A **Non-Fungible Token** (NFT) standard used to vest tokens ([EIP-20](./eip-20.m 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 ([EIP-20](./eip-20.md) or otherwise) that is emitted to the NFT holder. This standard is an extension of the [EIP-721](./eip-721.md) 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 of this, 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. +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 [EIP-20](./eip-20.md) token lock standard, but will also enable the creation of secondary marketplaces tailored for semi-liquid SAFTs. From 3b5e30a3711db3590a20048dd6968a120a3820aa Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 16 Nov 2022 12:35:46 -0300 Subject: [PATCH 18/22] refactor: Consistent style and event description --- EIPS/eip-5725.md | 14 ++++++++++---- assets/eip-5725/contracts/IERC5725.sol | 6 ++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 26e4e9a25bb5fd..104dfd7a645992 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -31,13 +31,13 @@ These curves could represent: ### Use Cases 1. 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. -2. Replicating SAFT contracts in a standardized form of semi-liquid vesting NFT assets +2. 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. -3. Providing a path for the standardization of vesting and token timelock contracts +3. 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. -4. NFT marketplaces dedicated to vesting NFTs +4. NFT marketplaces dedicated to vesting NFTs. - Whole new sets of interfaces and analytics could be created from a common standard for token vesting NFTs. -5. Integrating vesting NFTs into services like Gnosis Safe +5. Integrating vesting NFTs into services like Gnosis Safe. - A standard would mean services like Gnosis Safe could more easily and uniformly support interactions with these types of contracts inside of a multisig contract. 6. Enable standardized fundraising implementations and general fundraising that sell vesting tokens (eg. SAFTs) in a more transparent manner. 7. Allows tools, front-end apps, aggregators, etc. to show a more holistic view of the vesting tokens and the properties available to users. @@ -62,6 +62,12 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than alloted for a specific Vesting NFT. */ interface IERC5725 is IERC721 { + /** + * 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. + */ event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); /** diff --git a/assets/eip-5725/contracts/IERC5725.sol b/assets/eip-5725/contracts/IERC5725.sol index c4b3327cd3b0d2..22202926df85e3 100644 --- a/assets/eip-5725/contracts/IERC5725.sol +++ b/assets/eip-5725/contracts/IERC5725.sol @@ -10,6 +10,12 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; * tokens claimed per Vesting NFT so that a user cannot withdraw more tokens than alloted for a specific Vesting NFT. */ interface IERC5725 is IERC721 { + /** + * 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. + */ event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); /** From 23246ed941f7d44146c927aee1220f6eddd1e741 Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 16 Nov 2022 12:47:24 -0300 Subject: [PATCH 19/22] refactor: Implement new formatting lint recommendations --- EIPS/eip-5725.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 104dfd7a645992..791725fd801fc7 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -12,24 +12,27 @@ requires: 721 --- ## Abstract -A **Non-Fungible Token** (NFT) standard used to vest tokens ([EIP-20](./eip-20.md) or otherwise) over a vesting release curve. + +A **Non-Fungible Token** (NFT) standard used to vest tokens ([EIP-20](./eip-20.md) 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 ([EIP-20](./eip-20.md) or otherwise) that is emitted to the NFT holder. This standard is an extension of the [EIP-721](./eip-721.md) 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. + +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 [EIP-20](./eip-20.md) token lock standard, but will also enable the creation of secondary marketplaces tailored for semi-liquid SAFTs. +Such a standard will not only provide a much-needed [EIP-20](./eip-20.md) 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. +This standard also allows for a variety of different vesting curves to be implement easily. -These curves could represent: +These curves could represent: - linear vesting - cliff vesting - exponential vesting - custom deterministic vesting ### Use Cases + 1. 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. 2. 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. @@ -47,6 +50,7 @@ These curves could represent: ## 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. ```solidity @@ -138,19 +142,20 @@ interface IERC5725 is IERC721 { ## Rationale + **vesting terms** + - _vesting_: Tokens which are locked until a future date - _vested_: Tokens which have reached their unlock date. (The usage in this specification relates to the **total** vested tokens for a given Vesting NFT.) -- _claimable_: Amount of tokens which can be claimed at the current `timestamp`. -- _timestamp_: The unix `timestamp` (seconds) representation of dates used for vesting. +- _claimable_: Amount of tokens which can be claimed at the current `timestamp`. +- _timestamp_: The unix `timestamp` (seconds) representation of dates used for vesting. **vesting functions** + - `vestingPayout()` and `vestedPayout()` add up to the total number of tokens which can be claimed by the end of of the vesting schedule, which is also equal to `vestedPayoutAtTime()` with `type(uint256).max` as the `timestamp`. - `vestedPayout()` will provide the total amount of tokens which are eligible for release (including claimed tokens), while `claimablePayout()` provides the amount of tokens which can be claimed at the current `timestamp`. - `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. - - **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. @@ -166,22 +171,29 @@ The standard does not implement the following features: 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 [EIP-721](./eip-721.md) integrations and marketplaces. - The Vesting NFT standard also supports [EIP-165](./eip-165.md) 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 [eip-5725 assets](../assets/eip-5725/README.md/). ## 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. + +- 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 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. ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file + +Copyright and related rights waived via [CC0](../LICENSE.md). From 6331c89ec7b25ff394fc776af78bc0caa4e96bbd Mon Sep 17 00:00:00 2001 From: defifofum Date: Wed, 16 Nov 2022 16:20:37 -0600 Subject: [PATCH 20/22] refactor: Improve Rationale section --- EIPS/eip-5725.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 791725fd801fc7..6fe62e373ea444 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -143,25 +143,40 @@ interface IERC5725 is IERC721 { ## Rationale -**vesting terms** +### Terms -- _vesting_: Tokens which are locked until a future date +These are base terms used around the specification which function names and definitions are based on. + +- _vesting_: Tokens which are locked until a future date. - _vested_: Tokens which have reached their unlock date. (The usage in this specification relates to the **total** vested tokens for a given Vesting NFT.) - _claimable_: Amount of tokens which can be claimed at the current `timestamp`. - _timestamp_: The unix `timestamp` (seconds) representation of dates used for vesting. -**vesting functions** +### Vesting Functions -- `vestingPayout()` and `vestedPayout()` add up to the total number of tokens which can be claimed by the end of of the vesting schedule, which is also equal to `vestedPayoutAtTime()` with `type(uint256).max` as the `timestamp`. -- `vestedPayout()` will provide the total amount of tokens which are eligible for release (including claimed tokens), while `claimablePayout()` provides the amount of tokens which can be claimed at the current `timestamp`. -- `vestedPayoutAtTime()` provides functionality to iterate through the `vestingPeriod()` and provide a visual of the release curve. This allows for tools to iterate through a vesting schedule and create a visualization using on-chain data. +**`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`. + + +**`vestedPayout` vs `claimablePayout`** + +- `vestedPayout(uint256 tokenId)` will provide the total amount of tokens which are eligible for release **including claimed tokens**. +- `claimablePayout(uint256 tokenId)` provides the amount of tokens which can be claimed at the current `timestamp`. + +The rationale for providing two functions is so that the return of `vestedPayout(uint256 tokenId)` will always match the return of `vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)` with `block.timestamp` as the `timestamp`, and a separate function can be called to read how many tokens are available to claim. + +`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 -**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** +### Limitation of Scope The standard does not implement the following features: - Vesting Curves From 1596f2e380df6f864e0ad3735cd9b1e9a7a932f5 Mon Sep 17 00:00:00 2001 From: Apeguru Date: Wed, 16 Nov 2022 20:07:43 -0300 Subject: [PATCH 21/22] fix: MD linter errors --- EIPS/eip-5725.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 6fe62e373ea444..8c2aa2108f8348 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -26,6 +26,7 @@ Such a standard will not only provide a much-needed [EIP-20](./eip-20.md) token 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 @@ -174,11 +175,12 @@ The rationale for providing two functions is so that the return of `vestedPayout 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. +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 +### Limitation of Scope The standard does not implement the following features: + - Vesting Curves - Rental - Beneficiary From b1c8c4ed84503165aea8ff933ef2ec475a3446d9 Mon Sep 17 00:00:00 2001 From: defifofum Date: Fri, 13 Jan 2023 11:56:11 -0500 Subject: [PATCH 22/22] fix: Return value formatting --- EIPS/eip-5725.md | 2 +- assets/eip-5725/contracts/IERC5725.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5725.md b/EIPS/eip-5725.md index 6fe62e373ea444..e86baf6762e1f8 100644 --- a/EIPS/eip-5725.md +++ b/EIPS/eip-5725.md @@ -72,7 +72,7 @@ interface IERC5725 is IERC721 { * @param recipient The address which is receiving the payout. * @param _claimAmount The amount of tokens being claimed. */ - event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); + event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 claimAmount); /** * @notice Claim the pending payout for the NFT diff --git a/assets/eip-5725/contracts/IERC5725.sol b/assets/eip-5725/contracts/IERC5725.sol index 22202926df85e3..b44198a0609d78 100644 --- a/assets/eip-5725/contracts/IERC5725.sol +++ b/assets/eip-5725/contracts/IERC5725.sol @@ -16,7 +16,7 @@ interface IERC5725 is IERC721 { * @param recipient The address which is receiving the payout. * @param _claimAmount The amount of tokens being claimed. */ - event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 _claimAmount); + event PayoutClaimed(uint256 indexed tokenId, address indexed recipient, uint256 claimAmount); /** * @notice Claim the pending payout for the NFT