Skip to main content
info

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:

Timing parameters

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.
note

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 });