VestingToken.sol
VestingToken
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%.
underlyingToken
The ERC20 token that this contract will be vesting.
manager
The manager that deployed this contract which controls the values for fee
and feeCollector
.
_decimals
The decimals
value that is fetched from underlyingToken
.
_startingSupply
The initial supply used for calculating the claimableSupply
, claimedSupply
, and lockedSupply
.
_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.
_milestones
An array of Milestones describing the times and behaviour of the rules to release the vested tokens.
_lastReachedMilestone
Keep track of the last reached Milestone to minimize the iterations over the milestones and save gas.
_metadata
Maps a an address to the metadata needed to calculate claimableBalance
and lockedBalanceOf
.
Functions
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 aRamp.Linear
Milestone.*
Parameters
Name | Type | Description |
---|---|---|
name | string | This ERC20 token name. |
symbol | string | This ERC20 token symbol. |
underlyingTokenAddress | address | The ERC20 token that will be held by this contract. |
milestonesArray | Milestone[] | Array of all Milestone s 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.
addRecipient
Vests an amount
of underlyingToken
and mints LVTs for a recipient
.
Requirements:
msg.sender
must have approved this contract an amount ofunderlyingToken
greater or equal thanamount
.
Parameters
Name | Type | Description |
---|---|---|
recipient | address | The address that will receive the newly minted LVT. |
amount | uint256 | The amount of underlyingToken to be vested. |
addRecipients
Vests multiple amounts
of underlyingToken
and mints LVTs for multiple recipients
.
Requirements:
recipients
andamounts
must have the same length.msg.sender
must have approved this contract an amount ofunderlyingToken
greater or equal than the sum of all of theamounts
.
Parameters
Name | Type | Description |
---|---|---|
recipients | address[] | Array of addresses that will receive the newly minted LVTs. |
amounts | uint256[] | Array of amounts of underlyingToken to be vested. |
totalAmount | uint256 |
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 contractsunlockedPercentage
.claimableAmountOfImport
must be less than or equal than the amount that would be claimable given the values ofamount
andpercentage
.msg.sender
must have approved this contract an amount ofunderlyingToken
greater or equal thanamount
.*
Parameters
Name | Type | Description |
---|---|---|
recipient | address | The address that will receive the newly minted LVT. |
amount | uint256 | The amount of underlyingToken to be vested. |
claimableAmountOfImport | uint256 | The amount of underlyingToken from this transaction that should be considered claimable. |
unlocked | uint256 | The 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
, andclaimableAmountsOfImport
must have the same length.unlocked
must be less than or equal to this contractsunlockedPercentage
.- each value in
claimableAmountsOfImport
must be less than or equal than the amount that would be claimable given the values inamounts
andpercentages
. msg.sender
must have approved this contract an amount ofunderlyingToken
greater or equal than the sum of all of theamounts
.*
Parameters
Name | Type | Description |
---|---|---|
recipients | address[] | Array of addresses that will receive the newly minted LVTs. |
amounts | uint256[] | Array of amounts of underlyingToken to be vested. |
claimableAmountsOfImport | uint256[] | Array of amounts of underlyingToken from this transaction that should be considered claimable. |
totalAmount | uint256 | |
unlocked | uint256 | The unlocked percentage value at the time of the export of this transaction. |
exportRecipient
Parameters
Name | Type | Description |
---|---|---|
recipient | address | The address that will be exported. |
Returns
Name | Type | Description |
---|---|---|
<none> | address | The arguments to use in a call importRecipient on a different contract to migrate the recipient ’s metadata. |
<none> | uint256 | |
<none> | uint256 | |
<none> | uint256 |
exportRecipients
Parameters
Name | Type | Description |
---|---|---|
recipients | address[] | Array of addresses that will be exported. |
Returns
Name | Type | Description |
---|---|---|
<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.
Parameters
Name | Type | Description |
---|---|---|
startIndex | uint256 | Index of the Milestone we want the loop to start checking. |
unlockedPercentage
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The percentage of underlyingToken that users could claim. |
claimedSupply
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The amount of underlyingToken that were held in this contract and have been claimed. |
claimableSupply
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The amount of underlyingToken being held in this contract and that can be claimed. |
lockedSupply
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The amount of underlyingToken being held in this contract that can’t be claimed yet. |
claimedBalanceOf
Parameters
Name | Type | Description |
---|---|---|
account | address | The address whose tokens are being queried. |
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The amount of underlyingToken that were held in this contract and this account already claimed. |
claimableBalanceOf
Parameters
Name | Type | Description |
---|---|---|
account | address | The address whose tokens are being queried. |
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The amount of underlyingToken that this account owns and can claim. |
lockedBalanceOf
Parameters
Name | Type | Description |
---|---|---|
account | address | The address whose tokens are being queried. |
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | The 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.
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’.
Parameters
Name | Type | Description |
---|---|---|
amount | uint256 | Amount 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
.
Parameters
Name | Type | Description |
---|---|---|
to | address | Address of recipient. |
amount | uint256 | Amount 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
.
Parameters
Name | Type | Description |
---|---|---|
from | address | Address of sender. |
to | address | Address of recipient. |
amount | uint256 | Amount of tokens. |
milestones
Exposes the whole array of _milestones
.
metadataOf
Exposes the inner metadata for a given account.
Parameters
Name | Type | Description |
---|---|---|
account | address | The address whose tokens are being queried. |
transferFeeData
Returns the current transfer fee associated to this VestingToken
.
claimFeeData
Returns the current claim fee associated to this VestingToken
.
_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:
- 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: - claimableAmountAfterTransfer = claimableAmount ± claimableAmountOfTransfer
Notice the ± symbol is because the
sender
’sclaimableAmount
is reduced while thereceiver
’sclaimableAmount
is increased. - claimableAmountOfTransfer = claimableAmountOfSender * amountOfTransfer / balanceOfSender We can expand 3) into:
- 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 ReplacingclaimableAmountOfTransfer
in equation 2) and expanding it, we get: - claimableAmountAfterTransfer = ((unlockedPercentage * startingAmount) / ONE - claimedAmount) ± ((unlockedPercentage * startingAmountOfTransfer) / ONE - claimedAmountOfTransfer) We can group similar variables like this:
- 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 ofamountOfTransfer
, and the fee is a fraction of the amount, we can just factor in thetransferFeePercentage
to get the values for the transfer to thefeeCollector
. e) startingAmountOfFee = (startingAmountOfTransfer * transferFeePercentage) / ONE; f) claimedAmountOfFee = (claimedAmountOfTransfer * transferFeePercentage) / ONE; If we look at equation 1) and setunlockedPercentage
to ONE, thenclaimableAmount
must equal to thebalance
. Therefore the relation betweenstartingAmount
,claimedAmount
, andbalance
should be: g) startingAmount = claimedAmount + balance Since we want to minimize independent rounding in all of thestartingAmount
s, andclaimedAmount
s we will calculate theclaimedAmount
using multiplication and division as shown in b) and f), and thestartingAmount
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.*
Parameters
Name | Type | Description |
---|---|---|
from | address | Address of sender. |
to | address | Address of recipient. |
amount | uint256 | Amount of tokens. |
isTransfer | bool | If 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.
_tryFetchDecimals
Perform a staticcall to attempt to fetch underlyingToken
’s decimals. In case of an error, we default to
18.
_getBalanceOfThis
Perform a staticcall to attempt to fetch underlyingToken
’s balance of this contract. In case of an error,
reverts with custom UnsuccessfulFetchOfTokenBalance
error.
_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.
Parameters
Name | Type | Description |
---|---|---|
amount | uint256 | Amount of underlyingToken in the transaction. |
claimableAmountOfImport | uint256 | Amount of underlyingToken from this transaction that should be considered claimable. |
unlocked | uint256 | The unlocked percentage value at the time of the export of this transaction. |
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | Amount of underlyingToken that has been claimed based on the arguments given. |
_claimableAmount
Parameters
Name | Type | Description |
---|---|---|
startingAmount | uint256 | Amount of underlyingToken originally held. |
claimedAmount | uint256 | Amount of underlyingToken already claimed. |
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | Amount 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.
Events
Claim
Parameters
Name | Type | Description |
---|---|---|
account | address | Address that will receive the amount of underlyingToken . |
amount | uint256 | Amount of tokens that will be sent to the account . |
Burn
Parameters
Name | Type | Description |
---|---|---|
account | address | Address that will burn the amount of underlyingToken . |
amount | uint256 | Amount of tokens that will be sent to the dead address. |
MilestoneReached
Parameters
Name | Type | Description |
---|---|---|
milestoneIndex | uint256 | Index 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 claimedAmountAfterTransfer
s of every token
transfer the owner of account receives, claimedBalance
works as a counter for tokens claimed by this account.