Skip to content

Commit

Permalink
Update EIP-5725: Finalizing Draft (ethereum#6568)
Browse files Browse the repository at this point in the history
* chore: Commit draft EIP

* chore: ERC to EIP and add EIP number

* refactor: EIP Validator changes

* refactor: Relative EIP linking

* chore: updated author, discussions-to and removed various links

* chore: Moved reference implementation to assets

* chore: removed invalid EIP sections

* refactor: Remove comments from EIP-5725

Co-authored-by: Sam Wilson <[email protected]>

* refactor: Update SPDX license to CC0-1.0

* refactor: Rename IVestingNFT to IERC5725

* feat: Add EIP-5725 assets README

* fix: Links

* fix: Remove EIP-5725 header

* Apply suggestions from code review

Co-authored-by: Sam Wilson <[email protected]>

* refactor: Remove return value from claim()

* fix: EIP-N reference

* Update EIPS/eip-5725.md

Co-authored-by: Sam Wilson <[email protected]>

* refactor: Consistent style and event description

* refactor: Implement new formatting lint recommendations

* refactor: Improve Rationale section

* fix: MD linter errors

* fix: Return value formatting

* doc: Fix formatting

* review: ERC5725
- add claimedPayout function to support vested-but-locked tokens and update interface id
- improve definition of terms
- expand on rationale

* fix: lint

* fix: EIP -> ERC

---------

Co-authored-by: Apeguru <[email protected]>
Co-authored-by: Apeguru <[email protected]>
Co-authored-by: MarcoPaladin <[email protected]>
Co-authored-by: Sam Wilson <[email protected]>
  • Loading branch information
5 people authored Feb 27, 2023
1 parent 361352b commit e143cbb
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 64 deletions.
96 changes: 58 additions & 38 deletions EIPS/eip-5725.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ 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 ([ERC-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.
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](./eip-20.md) or otherwise) that is emitted to the NFT holder. This standard is an extension of the [ERC-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.

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 [ERC-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.

Expand All @@ -46,7 +46,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(InterfaceID)` check.
- Such tooling can easily discover compliance through the [ERC-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.


Expand All @@ -61,29 +61,45 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
/**
* @title Non-Fungible Vesting Token Standard
* @notice A non-fungible token standard used to vest tokens (EIP-20 or otherwise) over a vesting release curve
* @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 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);
* 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);
/**
* @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
* MUST emit PayoutClaimed
* SHOULD revert if there is nothing to claim
* @param tokenId The NFT token id
*/
function claim(uint256 tokenId) external;
/**
* @notice Number of tokens for the NFT which have been claimed at the current timestamp
* @param tokenId The NFT token id
* @return payout The total amount of payout tokens claimed for this NFT
*/
function claimedPayout(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()` subtracted from `payoutClaimed()`.
* @param tokenId The NFT token id
* @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed
*/
function claimablePayout(uint256 tokenId) external view returns (uint256 payout);
/**
* @notice Total amount of tokens which have been vested at the current timestamp.
* This number also includes vested tokens which have been claimed.
Expand All @@ -106,23 +122,13 @@ interface IERC5725 is IERC721 {
function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout);
/**
* @notice Number of tokens for an NFT which are currently vesting (locked).
* @notice Number of tokens for an NFT which are currently vesting.
* @dev The sum of vestedPayout and vestingPayout SHOULD always be the total payout.
* @param tokenId The NFT token id
* @return payout The number of tokens for the NFT which have not been claimed yet,
* regardless of whether they are ready to claim
* @return payout The number of tokens for the NFT which are vesting until a future date.
*/
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`.
Expand All @@ -148,9 +154,10 @@ interface IERC5725 is IERC721 {

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`.
- _vesting_: Tokens which a vesting NFT is vesting until a future date.
- _vested_: Total amount of tokens a vesting NFT has vested.
- _claimable_: Amount of vested tokens which can be unlocked.
- _claimed_: Total amount of tokens unlocked from a vesting NFT.
- _timestamp_: The unix `timestamp` (seconds) representation of dates used for vesting.

### Vesting Functions
Expand All @@ -159,15 +166,24 @@ These are base terms used around the specification which function names and defi

`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`.
The rationale for this is to guarantee that the tokens `vested` and tokens `vesting` are always in sync. The intent is that the vesting curves created are deterministic across the `vestingPeriod`. This creates useful opportunities for integration with these NFTs. For example: A vesting schedule can be iterated through and a vesting curve could be visualized, either on-chain or off-chain.


**`vestedPayout` vs `claimablePayout`**
**`vestedPayout` vs `claimedPayout` & `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`.
```solidity
vestedPayout - claimedPayout - claimablePayout = lockedPayout
```

- `vestedPayout(uint256 tokenId)` provides the total amount of payout tokens which have **vested** _including `claimedPayout(uint256 tokenId)`_.
- `claimedPayout(uint256 tokenId)` provides the total amount of payout tokens which have been unlocked at the current `timestamp`.
- `claimablePayout(uint256 tokenId)` provides the amount of payout tokens which can be unlocked at the current `timestamp`.

The rationale for providing three functions is to support a number of features:

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.
1. The return of `vestedPayout(uint256 tokenId)` will always match the return of `vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)` with `block.timestamp` as the `timestamp`.
2. `claimablePayout(uint256 tokenId)` can be used to easily see the current payout unlock amount and allow for unlock cliffs by returning zero until a `timestamp` has been passed.
3. `claimedPayout(uint256 tokenId)` is helpful to see tokens unlocked from an NFT and it is also necessary for the calculation of vested-but-locked payout tokens: `vestedPayout - claimedPayout - claimablePayout = lockedPayout`. This would depend on how the vesting curves are configured by the an implementation of this standard.

`vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)` provides functionality to iterate through the `vestingPeriod(uint256 tokenId)` and provide a visual of the release curve. The intent is that release curves are created which makes `vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)` deterministic.

Expand All @@ -179,26 +195,30 @@ The `timestamp` makes cross chain integration easy, but internally, the referenc

### Limitation of Scope

The standard does not implement the following features:
- **Historical claims**: While historical vesting schedules can be determined on-chain with `vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)`, historical claims would need to be calculated through historical transaction data. Most likely querying for `PayoutClaimed` events to build a historical graph.

### Extension Possibilities

These feature are not supported by the standard as is, but the standard could be extended to support these more advanced features.

- Vesting Curves
- Rental
- Beneficiary
- **Custom Vesting Curves**: This standard intends on returning deterministic `vesting` values given NFT `tokenId` and a **timestamp** as inputs. This is intentional as it provides for flexibility in how the vesting curves work under the hood which doesn't constrain projects who intend on building a complex smart contract vesting architecture.
- **Beneficiary**: This standard could be extended to provide for a `beneficiary` address who may `claim` unlocked tokens.
- **NFT Rentals**: Further complex DeFi tool can be created if vesting NFTs could be rented.

This is done intentionally to keep the base standard simple. These features can and likely will be added through extensions of this standard.

## Backwards Compatibility

- The Vesting NFT standard is meant to be fully backwards compatible with any current [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.
- The Vesting NFT standard is meant to be fully backwards compatible with any current [ERC-721](./eip-721.md) integrations and marketplaces.
- The Vesting NFT standard also supports [ERC-165](./eip-165.md) interface detection for detecting `ERC-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/).
A reference implementation of this EIP can be found in [ERC-5725 assets](../assets/eip-5725/README.md).

## Security Considerations

Expand Down
23 changes: 15 additions & 8 deletions assets/eip-5725/contracts/ERC5725.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ abstract contract ERC5725 is IERC5725, ERC721 {
* @param tokenId The NFT token id
*/
modifier validToken(uint256 tokenId) {
require(_exists(tokenId), "VestingNFT: invalid token ID");
require(_exists(tokenId), "ERC5725: invalid token ID");
_;
}

Expand All @@ -30,7 +30,7 @@ abstract contract ERC5725 is IERC5725, ERC721 {
function claim(uint256 tokenId) external override(IERC5725) validToken(tokenId) {
require(ownerOf(tokenId) == msg.sender, "Not owner of NFT");
uint256 amountClaimed = claimablePayout(tokenId);
require(amountClaimed > 0, "VestingNFT: No pending payout");
require(amountClaimed > 0, "ERC5725: No pending payout");

emit PayoutClaimed(tokenId, msg.sender, amountClaimed);

Expand Down Expand Up @@ -84,32 +84,39 @@ abstract contract ERC5725 is IERC5725, ERC721 {
/**
* @dev See {IERC5725}.
*/
function vestingPeriod(uint256 tokenId)
function claimedPayout(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 vestingStart, uint256 vestingEnd)
returns (uint256 payout)
{
return (_startTime(tokenId), _endTime(tokenId));
return _payoutClaimed[tokenId];
}

/**
* @dev See {IERC5725}.
*/
function payoutToken(uint256 tokenId)
function vestingPeriod(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (address token)
returns (uint256 vestingStart, uint256 vestingEnd)
{
return (_startTime(tokenId), _endTime(tokenId));
}

/**
* @dev See {IERC5725}.
*/
function payoutToken(uint256 tokenId) public view override(IERC5725) validToken(tokenId) returns (address token) {
return _payoutToken(tokenId);
}

/**
* @dev See {IERC165-supportsInterface}.
* IERC5725 interfaceId = 0xf8600f8b
* IERC5725 interfaceId = 0x7c89676d
*/
function supportsInterface(bytes4 interfaceId)
public
Expand Down
42 changes: 24 additions & 18 deletions assets/eip-5725/contracts/IERC5725.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,38 @@ import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
*/
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);
* 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);

/**
* @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
* MUST emit PayoutClaimed
* SHOULD revert if there is nothing to claim
* @param tokenId The NFT token id
*/
function claim(uint256 tokenId) external;

/**
* @notice Number of tokens for the NFT which have been claimed at the current timestamp
* @param tokenId The NFT token id
* @return payout The total amount of payout tokens claimed for this NFT
*/
function claimedPayout(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()` subtracted from `payoutClaimed()`.
* @param tokenId The NFT token id
* @return payout The amount of unlocked payout tokens for the NFT which have not yet been claimed
*/
function claimablePayout(uint256 tokenId) external view returns (uint256 payout);

/**
* @notice Total amount of tokens which have been vested at the current timestamp.
* This number also includes vested tokens which have been claimed.
Expand All @@ -49,23 +65,13 @@ interface IERC5725 is IERC721 {
function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp) external view returns (uint256 payout);

/**
* @notice Number of tokens for an NFT which are currently vesting (locked).
* @notice Number of tokens for an NFT which are currently vesting.
* @dev The sum of vestedPayout and vestingPayout SHOULD always be the total payout.
* @param tokenId The NFT token id
* @return payout The number of tokens for the NFT which have not been claimed yet,
* regardless of whether they are ready to claim
* @return payout The number of tokens for the NFT which are vesting until a future date.
*/
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`.
Expand Down

0 comments on commit e143cbb

Please sign in to comment.