VestingToken

Git Source

Inherits: ERC20Upgradeable, ReentrancyGuardUpgradeable, IVestingToken

Authors: JA (@ubinatus) v3, Klaus Hott (@Janther) v2

VestingToken locks ERC20 and contains the logic for tokens to be partially unlocked based on milestones.

State Variables

ONE

Percentages and fees are calculated using 18 decimals where 1 ether is 100%.

uint256 internal constant ONE = 1 ether;

underlyingToken

The ERC20 token that this contract will be vesting.

ERC20Upgradeable public underlyingToken;

manager

The manager that deployed this contract which controls the values for fee and feeCollector.

IFeeManager public manager;

_decimals

The decimals value that is fetched from underlyingToken.

uint8 internal _decimals;

_startingSupply

The initial supply used for calculating the claimableSupply, claimedSupply, and lockedSupply.

uint256 internal _startingSupply;

_importedClaimedSupply

The imported claimed supply is necessary for an accurate claimableSupply but leads to an improper offset in claimedSupply, so we keep track of this to account for it.

uint256 internal _importedClaimedSupply;

_milestones

An array of Milestones describing the times and behaviour of the rules to release the vested tokens.

Milestone[] internal _milestones;

_lastReachedMilestone

Keep track of the last reached Milestone to minimize the iterations over the milestones and save gas.

uint256 internal _lastReachedMilestone;

_metadata

Maps a an address to the metadata needed to calculate claimableBalance and lockedBalanceOf.

mapping(address => Metadata) internal _metadata;

Functions

constructor

constructor();

initialize

Initializes the contract by setting up the ERC20 variables, the underlyingToken, and the milestonesArray information.

*The Ramp of the first Milestone in the milestonesArray will always act as a Cliff since it doesn’t have a previous milestone. Requirements:

  • underlyingTokenAddress cannot be the zero address.
  • timestamps must be given in ascending order.
  • percentages must be given in ascending order and the last one must always be 1 eth, where 1 eth equals to 100%.
  • 2 percentages may have the same value as long as they are followed by a Ramp.Linear Milestone.*
function initialize(
    string calldata name,
    string calldata symbol,
    address underlyingTokenAddress,
    Milestone[] calldata milestonesArray
)
    external
    override
    initializer;

Parameters

NameTypeDescription
namestringThis ERC20 token name.
symbolstringThis ERC20 token symbol.
underlyingTokenAddressaddressThe ERC20 token that will be held by this contract.
milestonesArrayMilestone[]Array of all Milestones for this contract’s lifetime.

decimals

Returns the number of decimals used to get its user representation. For example, if decimals equals 2, a balance of 505 tokens should be displayed to a user as 5.05 (505 / 10 ** 2). Tokens usually opt for a value of 18, imitating the relationship between Ether and Wei. Since we can’t predict the decimals the underlyingToken will have, we need to provide our own implementation which is setup at initialization. NOTE: This information is only used for display purposes: it in no way affects any of the arithmetic of the contract.

function decimals() public view virtual override returns (uint8);

addRecipient

Vests an amount of underlyingToken and mints LVTs for a recipient. Requirements:

  • msg.sender must have approved this contract an amount of underlyingToken greater or equal than amount.
function addRecipient(address recipient, uint256 amount) external nonReentrant;

Parameters

NameTypeDescription
recipientaddressThe address that will receive the newly minted LVT.
amountuint256The amount of underlyingToken to be vested.

addRecipients

Vests multiple amounts of underlyingToken and mints LVTs for multiple recipients. Requirements:

  • recipients and amounts must have the same length.
  • msg.sender must have approved this contract an amount of underlyingToken greater or equal than the sum of all of the amounts.
function addRecipients(
    address[] calldata recipients,
    uint256[] calldata amounts,
    uint256 totalAmount
)
    external
    nonReentrant;

Parameters

NameTypeDescription
recipientsaddress[]Array of addresses that will receive the newly minted LVTs.
amountsuint256[]Array of amounts of underlyingToken to be vested.
totalAmountuint256

importRecipient

Behaves as addRecipient but provides the ability to set the initial state of the recipient’s metadata.

This functionality is included in order to allow users to restart an allocation on a different chain and keeping the inner state as close as possible to the original.

The Metadata.claimedAmountAfterTransfer for the recipient is inferred from the parameters.

The Metadata.claimedBalance is lost in the transfer, the closest value will be claimedAmountAfterTransfer.

In the rare case where the contract and it’s users are migrated after the last milestone has been reached, the claimedAmountAfterTransfer can’t be inferred and the claimedSupply value for the whole contract is lost in the transfer.

*The decision to do this is to minimize the altering of metadata to the amount that is being transferred and protect an attack that would render the contract unusable. Requirements:

  • unlocked must be less than or equal to this contracts unlockedPercentage.
  • claimableAmountOfImport must be less than or equal than the amount that would be claimable given the values of amount and percentage.
  • msg.sender must have approved this contract an amount of underlyingToken greater or equal than amount.*
function importRecipient(
    address recipient,
    uint256 amount,
    uint256 claimableAmountOfImport,
    uint256 unlocked
)
    external
    nonReentrant;

Parameters

NameTypeDescription
recipientaddressThe address that will receive the newly minted LVT.
amountuint256The amount of underlyingToken to be vested.
claimableAmountOfImportuint256The amount of underlyingToken from this transaction that should be considered claimable.
unlockeduint256The unlocked percentage value at the time of the export of this transaction.

importRecipients

Behaves as addRecipients but provides the ability to set the initial state of the recipient’s metadata.

This functionality is included in order to allow users to restart an allocation on a different chain and keeping the inner state as close as possible to the original.

The Metadata.claimedAmountAfterTransfer for each recipient is inferred from the parameters.

The Metadata.claimedBalance is lost in the transfer, the closest value will be claimedAmountAfterTransfer.

In the rare case where the contract and it’s users are migrated after the last milestone has been reached, the claimedAmountAfterTransfer can’t be inferred and the claimedSupply value for the whole contract is lost in the transfer.

The decision to do this to minimize the altering of metadata to the amount that is being transferred and protect an attack that would render the contract unusable.

*The Metadata for the recipient is inferred from the parameters. The decision to do this to minimize the altering of metadata to the amount that is being transferred. Requirements:

  • recipients, amounts, and claimableAmountsOfImport must have the same length.
  • unlocked must be less than or equal to this contracts unlockedPercentage.
  • each value in claimableAmountsOfImport must be less than or equal than the amount that would be claimable given the values in amounts and percentages.
  • msg.sender must have approved this contract an amount of underlyingToken greater or equal than the sum of all of the amounts.*
function importRecipients(
    address[] calldata recipients,
    uint256[] calldata amounts,
    uint256[] calldata claimableAmountsOfImport,
    uint256 totalAmount,
    uint256 unlocked
)
    external
    nonReentrant;

Parameters

NameTypeDescription
recipientsaddress[]Array of addresses that will receive the newly minted LVTs.
amountsuint256[]Array of amounts of underlyingToken to be vested.
claimableAmountsOfImportuint256[]Array of amounts of underlyingToken from this transaction that should be considered claimable.
totalAmountuint256
unlockeduint256The unlocked percentage value at the time of the export of this transaction.

exportRecipient

function exportRecipient(address recipient) external view returns (address, uint256, uint256, uint256);

Parameters

NameTypeDescription
recipientaddressThe address that will be exported.

Returns

NameTypeDescription
<none>addressThe arguments to use in a call importRecipient on a different contract to migrate the recipient’s metadata.
<none>uint256
<none>uint256
<none>uint256

exportRecipients

function exportRecipients(address[] calldata recipients)
    external
    view
    returns (address[] calldata, uint256[] memory, uint256[] memory, uint256);

Parameters

NameTypeDescription
recipientsaddress[]Array of addresses that will be exported.

Returns

NameTypeDescription
<none>address[]The arguments to use in a call importRecipients on a different contract to migrate the recipients’ metadata.
<none>uint256[]
<none>uint256[]
<none>uint256

updateLastReachedMilestone

This function will check and update the _lastReachedMilestone so the gas usage will be minimal in calls to unlockedPercentage.

This function is called by claim with a value of startIndex equal to the previous value of _lastReachedMilestone, but can be called externally with a more accurate value in case multiple Milestones have been reached without anyone claiming.

function updateLastReachedMilestone(uint256 startIndex) public;

Parameters

NameTypeDescription
startIndexuint256Index of the Milestone we want the loop to start checking.

unlockedPercentage

function unlockedPercentage() public view returns (uint256);

Returns

NameTypeDescription
<none>uint256The percentage of underlyingToken that users could claim.

claimedSupply

function claimedSupply() external view returns (uint256);

Returns

NameTypeDescription
<none>uint256The amount of underlyingToken that were held in this contract and have been claimed.

claimableSupply

function claimableSupply() public view returns (uint256);

Returns

NameTypeDescription
<none>uint256The amount of underlyingToken being held in this contract and that can be claimed.

lockedSupply

function lockedSupply() external view returns (uint256);

Returns

NameTypeDescription
<none>uint256The amount of underlyingToken being held in this contract that can’t be claimed yet.

claimedBalanceOf

function claimedBalanceOf(address account) external view returns (uint256);

Parameters

NameTypeDescription
accountaddressThe address whose tokens are being queried.

Returns

NameTypeDescription
<none>uint256The amount of underlyingToken that were held in this contract and this account already claimed.

claimableBalanceOf

function claimableBalanceOf(address account) public view returns (uint256);

Parameters

NameTypeDescription
accountaddressThe address whose tokens are being queried.

Returns

NameTypeDescription
<none>uint256The amount of underlyingToken that this account owns and can claim.

lockedBalanceOf

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

Parameters

NameTypeDescription
accountaddressThe address whose tokens are being queried.

Returns

NameTypeDescription
<none>uint256The amount of underlyingToken that this account owns but can’t claim yet.

claim

Claims available unlocked underlyingToken for the caller.

Transfers claimable amount to msg.sender and requires a claim fee (msg.value). Reverts if there’s no claimable amount. Protected against re-entrancy.

function claim() external payable nonReentrant;

burn

Allows an investor to burn their vested and underlying tokens.

First attempts to burn the underlying tokens. If unsuccessful, these are sent to address ‘0xdead’. This operation is followed by the burning of the equivalent vested tokens. Assumes the underlying token has a burn function with the selector ‘0x42966c68’.

function burn(uint256 amount) public payable;

Parameters

NameTypeDescription
amountuint256Amount of tokens to be burnt. The investor’s locked balance must be greater or equal than this amount.

transfer

Calculates and transfers the fee before executing a normal ERC20 transfer.

This method also updates the metadata in msg.sender, to, and feeCollector.

function transfer(address to, uint256 amount) public override returns (bool);

Parameters

NameTypeDescription
toaddressAddress of recipient.
amountuint256Amount of tokens.

transferFrom

Calculates and transfers the fee before executing a normal ERC20 transferFrom.

This method also updates the metadata in from, to, and feeCollector.

function transferFrom(address from, address to, uint256 amount) public override returns (bool);

Parameters

NameTypeDescription
fromaddressAddress of sender.
toaddressAddress of recipient.
amountuint256Amount of tokens.

milestones

Exposes the whole array of _milestones.

function milestones() external view returns (Milestone[] memory);

metadataOf

Exposes the inner metadata for a given account.

function metadataOf(address account) external view returns (Metadata memory metadata);

Parameters

NameTypeDescription
accountaddressThe address whose tokens are being queried.

transferFeeData

Returns the current transfer fee associated to this VestingToken.

function transferFeeData() external view returns (address, uint64);

claimFeeData

Returns the current claim fee associated to this VestingToken.

function claimFeeData() external view returns (address, uint64);

_updateMetadataAndTransfer

This function updates the metadata on the sender, the receiver, and the feeCollector if there’s any fee involved. The changes on the metadata are on the value claimedAmountAfterTransfer which is used to calculate _claimableAmount.

*The math behind these changes can be explained by the following logic:

  1. claimableAmount = (unlockedPercentage * startingAmount) / ONE - claimedAmount When there’s a transfer of an amount, we transfer both locked and unlocked tokens so the claimableAmountAfterTransfer will look like:
  2. claimableAmountAfterTransfer = claimableAmount ± claimableAmountOfTransfer Notice the ± symbol is because the sender’s claimableAmount is reduced while the receiver’s claimableAmount is increased.
  3. claimableAmountOfTransfer = claimableAmountOfSender * amountOfTransfer / balanceOfSender We can expand 3) into:
  4. claimableAmountOfTransfer = (unlockedPercentage * ((startingAmountOfSender * amountOfTransfer) / balanceOfSender)) / ONE) - ((claimedAmountOfSender * amountOfTransfer) / balanceOfSender) Notice how the structure of the equation is the same as 1) and 2 new variables can be created to calculate claimableAmountOfTransfer a) startingAmountOfTransfer = (startingAmountOfSender * amountOfTransfer) / balanceOfSender b) claimedAmountOfTransfer = (claimedAmountOfSender * amountOfTransfer) / balanceOfSender Replacing claimableAmountOfTransfer in equation 2) and expanding it, we get:
  5. claimableAmountAfterTransfer = ((unlockedPercentage * startingAmount) / ONE - claimedAmount) ± ((unlockedPercentage * startingAmountOfTransfer) / ONE - claimedAmountOfTransfer) We can group similar variables like this:
  6. claimableAmountAfterTransfer = (unlockedPercentage * (startingAmount - startingAmountOfTransfer)) / ONE - (claimedAmount - claimedAmountOfTransfer) This shows that the new values to calculate claimableAmountAfterTransfer if we want to continue using the equation 1) are: c) startingAmountAfterTransfer = startingAmount ± (startingAmountOfSender * amountOfTransfer) / balanceOfSender d) claimedAmountAfterTransfer = claimedAmount ± (claimedAmountOfSender * amountOfTransfer) / balanceOfSender Since these values depend linearly on the value of amountOfTransfer, and the fee is a fraction of the amount, we can just factor in the transferFeePercentage to get the values for the transfer to the feeCollector. e) startingAmountOfFee = (startingAmountOfTransfer * transferFeePercentage) / ONE; f) claimedAmountOfFee = (claimedAmountOfTransfer * transferFeePercentage) / ONE; If we look at equation 1) and set unlockedPercentage to ONE, then claimableAmount must equal to the balance. Therefore the relation between startingAmount, claimedAmount, and balance should be: g) startingAmount = claimedAmount + balance Since we want to minimize independent rounding in all of the startingAmounts, and claimedAmounts we will calculate the claimedAmount using multiplication and division as shown in b) and f), and the startingAmount can be derived using a simple subtraction. With this we ensure that if there’s a rounding down in the divisions, we won’t be leaving any token locked.*
function _updateMetadataAndTransfer(address from, address to, uint256 amount, bool isTransfer) internal;

Parameters

NameTypeDescription
fromaddressAddress of sender.
toaddressAddress of recipient.
amountuint256Amount of tokens.
isTransferboolIf a fee is charged, this will let the function know whether to use transfer or transferFrom to collect the fee.

_setupMilestones

Validates and initializes the VestingToken milestones.

It will perform validations on the calldata:

- Milestones have percentages and timestamps sorted in ascending order.

- No more than 2 consecutive Milestones can have the same percentage.

- 2 Milestones may have the same percentage as long as they are followed by a Milestone with a Ramp.Linear.

- Only the last Milestone should have 100% percentage.

function _setupMilestones(Milestone[] calldata milestonesArray) internal;

_tryFetchDecimals

Perform a staticcall to attempt to fetch underlyingToken’s decimals. In case of an error, we default to 18.

function _tryFetchDecimals() internal view returns (uint8);

_getBalanceOfThis

Perform a staticcall to attempt to fetch underlyingToken’s balance of this contract. In case of an error, reverts with custom UnsuccessfulFetchOfTokenBalance error.

function _getBalanceOfThis() internal view returns (uint256 returnedBalance);

_claimedAmount

This method is used to infer the value of claimed amounts.

If the unlocked percentage has already reached 100%, there’s no way to infer the claimed amount.

function _claimedAmount(
    uint256 amount,
    uint256 claimableAmountOfImport,
    uint256 unlocked
)
    internal
    pure
    returns (uint256);

Parameters

NameTypeDescription
amountuint256Amount of underlyingToken in the transaction.
claimableAmountOfImportuint256Amount of underlyingToken from this transaction that should be considered claimable.
unlockeduint256The unlocked percentage value at the time of the export of this transaction.

Returns

NameTypeDescription
<none>uint256Amount of underlyingToken that has been claimed based on the arguments given.

_claimableAmount

function _claimableAmount(uint256 startingAmount, uint256 claimedAmount) internal view returns (uint256);

Parameters

NameTypeDescription
startingAmountuint256Amount of underlyingToken originally held.
claimedAmountuint256Amount of underlyingToken already claimed.

Returns

NameTypeDescription
<none>uint256Amount of underlyingToken that can be claimed based on the milestones reached and initial amounts given.

_processClaimFee

Processes the claim fee for a transaction.

This function retrieves the claim fee data from the manager contract and, if the claim fee is greater than zero, sends the msg.value to the fee collector address. Reverts if the transferred value is less than the required claim fee or if the transfer fails.

function _processClaimFee() private;

Events

Claim

event Claim(address indexed account, uint256 amount);

Parameters

NameTypeDescription
accountaddressAddress that will receive the amount of underlyingToken.
amountuint256Amount of tokens that will be sent to the account.

Burn

event Burn(address indexed account, uint256 amount);

Parameters

NameTypeDescription
accountaddressAddress that will burn the amount of underlyingToken.
amountuint256Amount of tokens that will be sent to the dead address.

MilestoneReached

event MilestoneReached(uint256 indexed milestoneIndex);

Parameters

NameTypeDescription
milestoneIndexuint256Index of the Milestone reached.

Structs

Metadata

claimedAmountAfterTransfer is used to calculate the _claimableAmount of an account. It’s value is updated on every transfer, transferFrom, and claim calls.

While claimedAmountAfterTransfer contains a fraction of the claimedAmountAfterTransfers of every token transfer the owner of account receives, claimedBalance works as a counter for tokens claimed by this account.

struct Metadata {
    uint256 claimedAmountAfterTransfer;
    uint256 claimedBalance;
}