Skip to main content
info

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 of isTimed is false.
  • 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:

Timing parameters
  • The red cross on the left marks the point in time where the timing field is set.
  • 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 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 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.

  • 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 and cliffAmount 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 });