zkApp programmability is not yet available on the Mina Mainnet, but zkApps can now be deployed on the Mina Devnet.
Time-Locked Accounts
Time-locking allows you to pay someone in MINA or other custom 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.
By default, accounts are not time-locked.
The zkApp feature that enables time-locking is the timing
field that is present on every account:
type Account = {
// ...
timing: {
isTimed: Bool;
initialMinimumBalance: UInt64;
cliffTime: UInt32;
cliffAmount: UInt64;
vestingPeriod: UInt32;
vestingIncrement: UInt64;
};
};
- The
isTimed
field indicates whether this account is time-locked. The default value ofisTimed
isfalse
. - The other fields are parameters with default values that allow you to define a vesting schedule in a very flexible manner.
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. isTimed
switches fromfalse
totrue
.- 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 fromtrue
back tofalse
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 o1js
In o1js, 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 theinitialMinimumBalance
to ensure all tokens are unlocked when the cliff elapses.Both
vestingPeriod
andvestingIncrement
are unused, so set them to their default values,1
and0
:// 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.
Set the
vestingPeriod
to equivalent to 1 month defined in slots, so that new tokens are unlocked every month.Set the
vestingIncrement
to the total amount divided by 12, so that the total amount is unlocked after 12 months.Set both
cliffTime
andcliffAmount
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 });