Skip to content

Latest commit

 

History

History
345 lines (241 loc) · 15.2 KB

README.md

File metadata and controls

345 lines (241 loc) · 15.2 KB

Token Lockup and ERC20 Smart Contracts

WARNING: DO NOT DEPLOY THE main BRANCH TO A PRODUCTION ENVIRONMENT SUCH AS ETHEREUM MAINNET

The code in the main branch is under active development and there may be significant bugs or security issues introduced that have not been caught by code review or independent security auditors.

It is recommended you use versioned releases where there is an attached audit report. The independent audits are typically conducted for specific git commit identifiers specified in the security audit reports. It is highly advisable to perform your own audit of the smart contracts to both understand what you are deploying and to independently assess the security of the code.

This Open Source software is provided "as is" with no warranty as specified in the LICENSE file.

Overview

This is an Ethereum ERC-20 standard compatible token and TokenLockup scheduled release "vesting" smart contract that:

  • Does not have centralized controllers or admin roles to demonstrate strong decentralization and increased trust
  • Can enforce a scheduled release of tokens (e.g. investment lockups)
  • The maximum number of tokens is minted on deployment and it is not possible exceed this number
  • Smart contract enforced lockup schedules are used to control the circulating supply instead of inflationary minting.
  • Allows for burning tokens to reduce supply (e.g. for permanent cross chain transfers to a new blockchain and burning excess reserve tokens to support token price)
  • Optimized to decrease the use of gas for the costly transfer schedules

At A Glance

Feature Value
Network Ethereum / EVM Solidity
Protocol ERC-20
mint() no tokens minted ever after deployment
freeze() never
burn() Only from transaction senders own wallet address. No one can burn from someone else's address.
Admin Roles None
Upgradeable No
Transfer Restrictions None
Additional Functions Unlock Schedule related functions
Griefer Protection Minimum locked scheduled token amount slashing

Dev Environment

Clone this repo and cd into root. Then:

  • npm install to setup node libraries
  • npm test runs all tests and outputs code coverage and gas cost estimate (needs a Coinmarket cap private key for USD cost)
  • npm run coverage runs all tests
  • npm run fix runs the linter and fixes the standardjs lint offenses or npm run lint to lint without fixing

NatSpec docs are regenerated by HardHat docgen plugin each time the smart contracts are compiled with hardhate. You can view the html version of the docs in a browser with:

open docs/index.html

Deployment

Run npx hardhat run scripts/scriptName.js --network networkName for each [scriptName] in the scripts repository. The scripts should be run in numeric sequence.

You will need to update the hardhat.config.js file the lockup tokenAddress for the the token that should be used in the TokenLockup contract.

Token Smart Contract

ERC-20 Token Interface

The token implements the ERC-20 token standard that conforms to this interface:

interface IERC20 {
  function totalSupply() external view returns (uint256);

  function balanceOf(address who) external view returns (uint256);

  function allowance(address owner, address spender)
    external view returns (uint256);

  function transfer(address to, uint256 value) external returns (bool);

  function approve(address spender, uint256 value)
    external returns (bool);

  function transferFrom(address from, address to, uint256 value)
    external returns (bool);

  event Transfer(
    address indexed from,
    address indexed to,
    uint256 value
  );

  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
  );
}

Use Of Common Extended ERC-20 Token Functions

In addition to the standared ERC-20 functions, the token will implement the extended ERC-20 functions for:

function name() public view returns (string memory);
function symbol() public view returns (string memory);
function decimals() public view returns (uint8);

Only Can Call Burn Own Tokens

The burn function can only be applied to the msg.sender account. This follows the principle that there are no special contract roles that could burn another token holders tokens.

function burn(uint256 amount) external;

Only Decreasing Token Supply

All tokens are minted on deployment. mint() cannot be called after deployment. This means that the ERC20 totalSupply() can only remain constant or decrease when accounts call burn() on their own tokens.

TokenLockup Smart Contract

Lockup Schedules Control Circulating Supply Instead Of Minting

Smart contract enforced lockup schedules are used to control the circulating supply instead of minting. Lockups are applied to investors and other token holders at the time of transferring tokens.

The lockup period implementation lowers gas fees by referring to common release schedule tables and using unlock calculations that do not require updating smart contract state for time dependent lockups.

Define A Release Schedule

Lockup period schedules may be configured and funded without a central admin role and from any address. This empowers reserve managers, crowdfunding portals and others to enforce on chain lockup schedules.

Anyone can create release schedule. Schedules can be reused with different commencement dates and amounts.

function createReleaseSchedule(
    uint releaseCount, // total number of releases including any initial "cliff'
    uint delayUntilFirstReleaseInSeconds, // "cliff" or 0 for immediate relase
    uint initialReleasePortionInBips, // in 100ths of 1%
    uint periodBetweenReleasesInSeconds
)
external
returns (uint unlockScheduleId)

When a release schedule is created it emits an event with the scheduleId

event ScheduleCreated(address indexed from, uint scheduleId);

Implementation Details

  • The date is in unix timestamp format. The unlock time granularity is intended to be days roughly. The roughly 900 second blocktime variance for Ethereum block timestamp should be expected. However it is not an issued for a time specificity tolernace of roughly days.
  • The percentage is stored as 100ths of a percent - bips. The maximum specificity is multiple of 0.0001 represented as uint 1 bip.

Example Release Schedule

Here's an example of creating a release schedule using the Ethereum Ethers.js library:

await tokenLockup.connect(reserveAccount).createReleaseSchedule(
    4, // total number of releases including any initial "cliff'
    0, // 0 time delay until first release (immediate release)
    800, // the initial portion released in 100ths of 1%
    (90 * 24 * 60 * 60) // time between releases expressed in seconds = 90 days
)

// returns id 1 after creating the release schedule

This is an example of funding the release schedule for a specific recipient:

await tokenLockup.connect(reserveAccount).fundReleaseSchedule(
    recipient.address,
    100,
    Math.floor(Date.now() / 1000), // the commencement date unix timestamp in seconds
    1 // scheduleId
)

Funding A Release Schedule

A transfer can reference a release scheduleId to fund for a recipient. The release schedule controls when tokens will be unlocked calculated from a commencementTimestamp in the past, present or future. This flexible scheduling allows reuse of schedules for promises that may have been made during project formation, a funding event or that exist in legal documents.

function fundReleaseSchedule(
    address to,
    uint amount,
    uint commencementTimestamp, // unix timestamp
    uint scheduleId,
    address[] memory cancelableBy
) public returns (bool success)

Cancelable Timleock "Vesting"

If the release schedule is not cancelable (like investor lockups that cannot be reclaimed from the investor) then the fundReleaseSchedule(..., address[] memory cancelableBy) should just be an empty array [].

If the release schedule is cancelable (like employee token vesting conditional on employment) then fundReleaseSchedule(..., address[] memory cancelableBy) should include the addresses that can cancel the recipients timelock to reclaim the remaining locked tokens. The tokens that are unlocked cannot be reclaimed, they can only be transferred by the recipient address.

Vesting Cancelation Example

GIVEN the release schedule was funded with

tokenLockup.fundReleaseSchedule(
    recipientAddress,
    100,        // amount
    yesterday,  // commencementDate
    1,          // schedule id 
    [cancelorAddress]
);

AND today the timelock's release schedule 1 dictates that 51 of the 100 tokens are unlocked AND 49 of the tokens are still locked WHEN the cancelorAddress owner cancels the timelock with

tokenLockup.cancelTimelock(
        recipientAddress,
        timelockIndex,
        reclaimTokenToAddress // where reclaimed tokens will go
    );

THEN the 51 unlocked tokens are transferred to the recipientAddress AND the 49 locked tokens are transferred to the reclaimTokenToAddress AND the timelock.tokensTransferred == timelock.totalAmount AND the number of funded tokens still in the contract is 0

There are a lot more usage examples in the tests folder.

Transferring Unlocked Tokens

In the release schedule funding example above, tokens can be transfered by the recipient account using the transfer() function. Their tokens are unlocked on this schedule:

Release Schedule Percentage (bips) Release # Amount
2021-06-01 (commencementDate
+ 0 delayUntilFirstReleaseInSeconds)
8% 1 8
+ 90 days 30.66% 2 30.66
+ 180 days 30.66% 3 30.66
+ 270 days 30.66% + 0.01 remainder 4 30.67
Total 100% 100

Remainders

In the process of calculating the lockup, some rounding errors may occur. These rounding remainder amounts are typically of very small value with a token between 8 and 18 decimal places.

To unlock the exact number of tokens needed for the final lockup period in the schedule, the final scheduled amount is for all tokens that have not yet been unlocked in the unlock schedule.

Transferring Released Tokens

Transfers can be done with an ERC20 style transfer interface that will transfer all unlocked tokens for all schedules belonging to the message sender.

transfer(to, amount)
  • the transfer() and transferFrom() have the standard ERC20 interface to make it easy to use MetaMask and other tooling.
  • Lockup periods are checked and enforced for any transfer function call.

Checking Total Balances

These functions allow you to check total locked and unlocked tokens for an address:

function balanceOf(address who) external view returns (uint256);

Check just the locked tokens for an address:

function lockedBalanceOf(address who) external view returns (uint256);

Check just the unlocked tokens for an address:

function unlockedBalanceOf(address who) external view returns (uint256);

Check the total number of tokens stored in the smart contract:

function totalSupply() external view returns (uint256);

Specific Release Schedule Balances

Check total locked and unlocked tokens for an address:

function viewTimelock(address who, index) external view returns (uint amount, uint scheduleId, uint commencementDate, uint unlockedBalance, uint lockedBalance);

Griefer Protection

"Griefing" is bad faith use of a system to enrage, troll or cause damage to other users. The contract has no centralized control and implements self service functions to avoid possible griefing attacks by other contract users.

The primary predicted griefing attack vector would be overloading recipients with spam release schedule timelocks. To avoid this the contract makes this attack costly. To avoid this issue the following protections are available.

Minimimum Release Schedule Amount

To avoid increasing computation requirements, gas cost for transfers and exceeding max gas for a transaction, each transferWithRelease schedule amount must be for an amount of tokens > minReleaseScheduleAmount.

Each release period must also release at least one token. Release periods can be as small as one second since they are calculated and do not require storage updates on the blockchain until the time of transfer.

Individual Schedule Transfer

To avoid the possibility that a a recipient might have too many release schedules to calculate in the transfer function, individual release schedules can be separately transferred with:

function transferTimelock(address to, uint value, uint timelockId) public returns (bool) 

Gas Optimization

To reduce gas fees, reusable schedules are referenced by a single ID. Unlocked tokens are calculated using a formula. This keeps each transfer from requiring it's own vesting schedule data storage and drops the number of SSTORE values required.

Batch Transfers

Batch transfer functions can significantly lower the cost of making many transfers.

There is a batchTransfer function suitable for use with any ERC20 token in the BatchTransfer.sol

function batchTransfer(IERC20 token, address[] memory recipients, uint[] memory amounts) external returns (bool) 

And a batchFundReleaseSchedule() is part of the TokenLockup.sol contract:

function batchFundReleaseSchedule(
    address[] memory to,
    uint[] memory amounts,
    uint[] memory commencementTimestamps,
    uint[] memory scheduleIds,
    address[] memory cancelableBy
) external returns (bool success)

Deployment

Deployment scripts are provided in the scripts folder. These scripts can be run with

npx hardhat clean && npx hardhat run scripts/1_deploy-token.js

The scripts also publish contract definitions to Etherscan where users can interact with the verified smart contract definitions using MetaMask wallets.