Please note that zkApp programmability is not yet available on Mina Mainnet, but zkApps can now be deployed to Berkeley Testnet.
Time-Locked Accounts
Time-locking allows you to pay someone in MINA or custom other tokens subject to a vesting schedule. Tokens are initially locked and become available for withdrawal only after a certain time or gradually according to a specific schedule.
The zkApp feature that enables time-locking is the timing
field that is present on every account. It look like this:
type Account = {
// ...
timing: {
isTimed: Bool;
initialMinimumBalance: UInt64;
cliffTime: UInt32;
cliffAmount: UInt64;
vestingPeriod: UInt32;
vestingIncrement: UInt64;
};
};
isTimed
indicates whether this account is time locked. The other fields are parameters that allow you to define a vesting schedule in a very flexible manner. By default, accounts are not time locked. The default value of isTimed
is false
, and all other properties contain default values.
This graph shows how each of the timing properties affect the vesting schedule:
The red cross on the left marks the point in time where the timing
field is set and isTimed
switches from false
to true
. The orange line shows how the amount of unlocked tokens increases over time until it finally reaches its maximum value and stays flat. At this point, isTimed
flips from true
back to false
because no tokens remain locked.
As shown, the maximum amount of unlocked tokens is defined by the initialMinimumBalance
. The property is called "initialMinimumBalance" because, even though the tokens show up in the balance, they can't be withdrawn. The account has a a non-zero minimum balance. Initially, that minimum balance is equal to the amount of tokens locked -- so, that amount is the "initial minimum balance". Over time, the minimum balance decreases until it hits zero, which is the condition that makes isTimed
false again.
The other timing-related properties are:
cliffTime
: The initial time period during which all tokens are locked. Note that 'time' is measured in Mina by 'slots', where 1 slot is 3min.cliffAmount
: The quantity of tokens to be unlocked when the cliff time has elapsed. If this amount is greater or equal the 'initial minimum balance', all tokens are unlocked after the cliff time elapses.vestingPeriod
: After the cliff time elapses, tokens can be set to unlock periodically at a fixed interval, by a fixed quantity. The vesting period is the length of that interval.vestingIncrement
: The quantity of tokens that are unlocked after each vesting period elapses.
Only one vesting schedule can be specified per account. The vesting schedule cannot be changed during the vesting period.
Because of this restriction, the values of the timing fields cannot be changed when isTimed
is set to true
.
After all tokens are unlocked and isTimed
flips back to false
, the account timing becomes mutable again.
Setting timing in SnarkyJS
In SnarkyJS, timing
is one of the account fields that can be updated by using an account update:
accountUpdate.account.timing.set({ initialMinimumBalance, cliffTime, ...etc });
When setting timing, all timing-related properties are required, except for isTimed
which is automatically set by the protocol.
Examples
These examples show how to correctly implement several example use cases.
Example 1: All tokens unlock after 1 week
If you want all tokens to unlock after a certain time, then the only properties you need to consider are initialMinimumBalance
, cliffTime
, and cliffAmount
. Set cliffAmount
equal to the initialMinimumBalance
to ensure all tokens are unlocked when the cliff elapses. Both vestingPeriod
and vestingIncrement
are unused so set them to their default values, 1 and 0:
// example: 10 MINA to lock
const tokensToLock = UInt64.from(10e9);
// calculate 1 week in slots
const cliffTime = UInt32.from((60 / 3) * 24 * 7);
accountUpdate.account.timing.set({
initialMinimumBalance: tokensToLock,
cliffTime,
cliffAmount: tokensToLock,
vestingPeriod: UInt32.from(1), // 0 is not allowed; default value is 1
vestingIncrement: UInt64.from(0),
});
this.send({ to: accountUpdate, amount: tokensToLock });
Example 2: Linear vesting over 1 year
This example does not use a cliff but vests a certain number of tokens linearly over 1 year. To do this, set the vestingPeriod
to equivalent to 1 month defined in slots, so that new tokens are unlocked every month. The vestingIncrement
is set to the total amount divided by 12, so that the total amount is unlocked after 12 months. Both cliffTime
and cliffAmount
can just be set to 0.
// example: 100000 MINA to lock
const tokensToLock = UInt64.from(100000e9);
// calculate 1 month in slots
const vestingPeriod = UInt32.from(Math.round(((60 / 3) * 24 * 365) / 12));
// 1/12th of tokens unlocked every month
const vestingIncrement = UInt64.from(Math.round(tokensToLock / 12));
accountUpdate.account.timing.set({
initialMinimumBalance: tokensToLock,
cliffTime: UInt32.from(0),
cliffAmount: UInt64.from(0),
vestingPeriod,
vestingIncrement,
});
this.send({ to: accountUpdate, amount: tokensToLock });