Skip to content

Synthetixio/tokenvest

Repository files navigation

Tokenvest

Note: The front-end now relies on Multicall3. Set NEXT_PUBLIC_MULTICALL_ADDRESS accordingly.

Overview

This is a smart contract and dApp that manages token grants with vesting schedules.

Each grant is represented as an NFT. It has the following properties:

  • tokenAddress - The address of the tokens being provided by the grant.
  • startTimestamp - The timestamp (in seconds) when the grant begins to vest.
  • cliffTimestamp - The timestamp (in seconds) before which no tokens can be redeemed.
  • vestInterval - The duration (in seconds) for each additional amount to vest.
  • vestAmount - The amount of tokens that will vest each interval.
  • totalAmount - The total amount of tokens that will vest for this grant.
  • amountRedeemed - The amount of tokens already redeemed under this grant.
  • cancelled - Whether this grant was cancelled by the contract, this is the only mutable property.

The contract owner is able to manage grants using the mint(), cancelGrant(), and replaceGrant() functions. The owner can also withdraw tokens using the withdraw() function. The dApp assumes this will be a multisig wallet on Gnosis Safe. Ownership can be transferred using the nominateOwner() and acceptOwnership() functions.

Holders of the NFT are able to redeem available tokens using the redeem(), redeemMultiple(), redeemAll(), or redeemWithTransfer() methods.

Development Environment

  • Run npx hardhat node
  • In a separate tab, npx hardhat run --network localhost scripts/local-deploy.js
  • Then start the front end, cd frontend && npm run dev (ensure to npm i here as well). Owner is HH wallet 0, grantee is HH wallet 1.

Technical Specification

This an ERC-721 contract that implements the enumerable extension. Each NFT corresponds to a grant and allows the grantee to redeem ERC-20 tokens from the contract according to a specified vesting scheduling.

Owner

  • The contract has a single owner, initialized in the constructor.
  • Ownership can be transferred by nominating a new owner using the nominateOwner() function. The nominated owner can then claim ownership by calling acceptOwnership(). Only the current owner should be able to nominate a new owner. Only the nominated owner (nominatedOwner) should be able to accept ownership.
  • The owner is expected to supply tokens being granted with this contract (tokenAddress) to the contract using supply(), though anyone could transfer any tokens to this contract.
  • The owner, and only the owner, should be able to withdraw all of any token from the contract using the withdraw() method.
  • The owner, and only the owner, can issue a new grant using the mint() function, specifying the grantee's address and all of the properties in the Grant struct. tokenCounter increments such that each grant always has a unique ID.
  • The owner, and only the owner, can replace a grant based on it's token ID. This should mint a replacement grant with new attributes to the previous grantee.
  • The owner, and only the owner, should be able to cancel a grant with the cancelGrant() function. No one, including the grantee assigned to that grant, should be able to redeem tokens based on a grant that has been cancelled.

Grantee

  • The grantee (and only the grantee) should be able to redeem tokens from the contract according to the vesting schedule associated with the NFTs they hold.
    • Starting at the startTimestamp, each vestInterval should vest vestAmount of tokens.
    • No tokens should vest prior to the cliffTimestamp, but this should not be taken into account when calculating the amount vested after the cliff has passed. For example, given a vestAmount of 100, a vestInterval of 3 months, and a cliffTimestamp of +6 months, 0 tokens should be vested at +5 months, 200 tokens should be vested at +6 months, and 300 tokens should be vested at +9 months.
    • Under no circumstances should a grant provide access to more than the totalAmount of tokens.
    • Users are able to redeem the amount of vested tokens minus the amount of tokens they've already redeemed.
  • The grantee (and only the grantee) should be able to redeem tokens (as specified above) and also transfer any amount of any ERC-20 token to this contract in a single transaction using the redeemWithTransfer() function.
  • The grantee should be able to transfer that grant to another address using the standard functions in the ERC-721 specification.