In part 1 we described the rationale behind the MNDCC token and its integrated vesting schedule. In short: It locks your tokens and releases a portion of them after every month over a period of 29 months. The major constraints are:
- All token holders are subject to the same vesting schedule
- The vesting start date is different for each token holder.
- No one can change the vested amount or start date.
⚠️ Solidity Code Ahead…
This 2nd part focuses on the technical aspects of the ERC20 contract implementation, especially how it prevents transfers of tokens if they exceed the amount released by the vesting schedule.
We’ll align the technical discussion with the typical events in a user’s journey:
- Buy tokens in private sale.
- Receive tokens from the contract’s owner, subject to the vesting schedule. The vesting period usually starts at the time of purchase.
- See the balance of free tokens (released by vesting) in an ERC20 compatible wallet.
- Send free tokens.
We decided to rely on existing, audited, battle tested code as much as possible, even if it might not lead to the most gas efficient solution. The MNDCC contract extends the vanilla ERC20 contract from OpenZeppelin.
1. Buy tokens
This happens in a private sale. For each buyer we store the amount of MNDCC bought and the time of purchase in our database.
2. Receive Vested Tokens
The contract interaction starts with sending the purchased tokens to the buyer by calling
The method eventually delegates to the vanilla
_transfer to transfer the tokens from the contract owner (who holds all of them initially) to the buyer. Before that it stores the vesting details of the buyer, i.e the amount of MNDCC bought and the time of purchase from step 1, and emits a corresponding event.
This helps to keep the default implementation untouched as you’ll see next.
3. See Balance of Free Tokens
The contract’s main responsibility is to calculate the amount of available tokens (released by vesting) for any given time. The ERC20 method
balanceOf usually returns the current token balance. We must override that method to not return all tokens, but only the amount that remains after subtracting the tokens that haven’t been released yet from all tokens.
To understand the
_amountAvailable method, let’s look at the contract constructor first:
Apart from the usual suspects
totalSupply the constructor expects 2 arrays that represent the vesting schedule:
_vestingDays (3–5)takes the cumulative number of seconds for each 30-day period.
_vestingBasisPoints (7-9)has the same length and contains the corresponding cumulative basis points released in that month.
Equipped with these 2 data structures we can now break down the core
The first step is to determine whether the sender is subject to vesting or not (
2–4), using the
vesting mapping from step 1 and get the sender’s
totalBalance which is the balance tracked by the ERC20 default implementation.
Next we find out how far the sender has come in the vesting schedule. We reuse OpenZeppelin’s Arrays utility function
findUpperBound to find the correct index in the
_vestingDays array and pass the days that have past since the vesting started for that particular sender (
Should the sender still be subject to vesting (period has not yet ended), we get to the gist of the method:
10Get the basis points for the identified vesting slot.
11–14Calculate the maximum amount that can be spent from the vested tokens by multiplying the initial vested amount with the basis points from the vesting slot.
15-16Calculate the amount that is still locked by vesting.
17Subtract the locked amount from the sender’s total balance and return that.
4. Send Free Tokens
_amountAvailable method in place, restricting the transferable amount is easy by overriding the default implementation’s
_beforeTokenTransfer abstract method.
By not fiddling with the OpenZeppelin’s ERC20 implementaion and by precomputing the vesting arrays, the need for custom code and accompanying complexity reduces significantly.
Only one vesting per holder (address) is supported. Should a buyer make multiple purchases, a different address has to be used.
There’s no need to actively free any tokens which saves us from operational headaches and gas expenses.
As a convenience for users who also want to see the total token balance (including the yet locked tokens) in an ERC20 compatible wallet, we deployed a 2nd ERC20 contract as a proxy, whose
balanceOf method returns the original contract’s total balance.