Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Commit draft EIP #2

Merged
merged 27 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
70bb638
chore: Commit draft EIP
Apegurus Sep 28, 2022
bfc1e1f
chore: ERC to EIP and add EIP number
Apegurus Sep 28, 2022
1d9961a
refactor: EIP Validator changes
Apegurus Sep 28, 2022
81feac0
refactor: Relative EIP linking
Apegurus Sep 28, 2022
e051390
chore: updated author, discussions-to and removed various links
JorgeAtPaladin Sep 30, 2022
11e64dc
chore: Moved reference implementation to assets
JorgeAtPaladin Sep 30, 2022
3006271
chore: removed invalid EIP sections
JorgeAtPaladin Sep 30, 2022
48415ba
Merge pull request #3 from JorgeAtPaladin/draft
Apegurus Oct 2, 2022
068abab
refactor: Remove comments from EIP-5725
DeFiFoFum Oct 4, 2022
3a8ee84
refactor: Update SPDX license to CC0-1.0
DeFiFoFum Oct 4, 2022
761abc6
refactor: Rename IVestingNFT to IERC5725
DeFiFoFum Oct 4, 2022
f15a541
feat: Add EIP-5725 assets README
DeFiFoFum Oct 4, 2022
4cb049c
Merge branch 'draft' into rebrand/ERC5725
DeFiFoFum Oct 4, 2022
1118b8d
fix: Links
DeFiFoFum Oct 4, 2022
f89cd06
fix: Remove EIP-5725 header
DeFiFoFum Oct 4, 2022
3781d99
Apply suggestions from code review
Apegurus Oct 5, 2022
c62aebe
Merge pull request #4 from ApeSwapFinance/rebrand/ERC5725
DeFiFoFum Oct 5, 2022
f84d07c
refactor: Remove return value from claim()
DeFiFoFum Oct 7, 2022
6b60f17
fix: EIP-N reference
DeFiFoFum Oct 17, 2022
a491393
Update EIPS/eip-5725.md
Apegurus Nov 16, 2022
3b5e30a
refactor: Consistent style and event description
Apegurus Nov 16, 2022
23246ed
refactor: Implement new formatting lint recommendations
Apegurus Nov 16, 2022
6331c89
refactor: Improve Rationale section
DeFiFoFum Nov 16, 2022
aa09fae
Merge pull request #5 from ApeSwapFinance/dev
Apegurus Nov 16, 2022
1596f2e
fix: MD linter errors
Apegurus Nov 16, 2022
b1c8c4e
fix: Return value formatting
DeFiFoFum Jan 13, 2023
4a2437a
Merge branch 'dev' into draft
DeFiFoFum Jan 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions EIPS/eip-5725.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
eip: 5725
title: Transferable Vesting NFT
description: An interface for transferable vesting NFTs which release underlying tokens over time.
author: Apeguru (@Apegurus), Marco De Vries <marco@paladinsec.co>, Mario <mario@paladinsec.co>, DeFiFoFum (@DeFiFoFum)
discussions-to: https://ethereum-magicians.org/t/eip-5725-transferable-vesting-nft/11099
status: Draft
type: Standards Track
category: ERC
created: 2022-09-08
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.

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.

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.

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](./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.


## 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: CC0-1.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 (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.
*/
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);

/**
* @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
*/
function claim(uint256 tokenId) external;

/**
* @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

### Terms

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

**`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

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

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

- 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.
- `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).
8 changes: 8 additions & 0 deletions assets/eip-5725/README.md
Original file line number Diff line number Diff line change
@@ -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/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.
156 changes: 156 additions & 0 deletions assets/eip-5725/contracts/ERC5725.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: CC0-1.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 "./IERC5725.sol";

abstract contract ERC5725 is IERC5725, 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 {IERC5725}.
*/
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");

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

_payoutClaimed[tokenId] += amountClaimed;
IERC20(payoutToken(tokenId)).safeTransfer(msg.sender, amountClaimed);
}

/**
* @dev See {IERC5725}.
*/
function vestedPayout(uint256 tokenId) public view override(IERC5725) returns (uint256 payout) {
return vestedPayoutAtTime(tokenId, block.timestamp);
}

/**
* @dev See {IERC5725}.
*/
function vestedPayoutAtTime(uint256 tokenId, uint256 timestamp)
public
view
virtual
override(IERC5725)
returns (uint256 payout);

/**
* @dev See {IERC5725}.
*/
function vestingPayout(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 payout)
{
return _payout(tokenId) - vestedPayout(tokenId);
}

/**
* @dev See {IERC5725}.
*/
function claimablePayout(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
returns (uint256 payout)
{
return vestedPayout(tokenId) - _payoutClaimed[tokenId];
}

/**
* @dev See {IERC5725}.
*/
function vestingPeriod(uint256 tokenId)
public
view
override(IERC5725)
validToken(tokenId)
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
*/
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(ERC721, IERC165)
returns (bool supported)
{
return interfaceId == type(IERC5725).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);
}
Loading