简介与要求

本规范定义了 Cosmos Hub 使用的归属账户实现。该归属账户的要求是在创世阶段使用初始余额 X 和归属结束时间 ET 进行初始化。归属账户还可以使用归属开始时间 ST 和若干归属周期 P 进行初始化。如果包含归属开始时间,则在到达开始时间之前不会启动归属周期。如果包含归属周期,则归属会按照指定的周期数逐步发生。 对于所有归属账户,账户所有者都可以向验证者发起委托和取消委托,但在代币完成归属之前,不能将其转移到其他账户。本规范支持四种不同的归属方式:
  • 延迟归属:当到达 ET 时,所有代币一次性完成归属。
  • 连续归属:代币从 ST 开始归属,并随时间线性归属,直到到达 ET。
  • 周期性归属:代币从 ST 开始归属,并按照周期数和每个周期的归属数量定期归属。周期数、每个周期的长度以及每个周期的数量都可配置。周期性归属账户与连续归属账户的区别在于,代币可以按分批方式释放。例如,周期性归属账户可用于按季度、按年,或按任何其他基于时间的代币释放安排。
  • 永久锁定归属:代币会被永久锁定。此类账户中的代币即使处于锁定状态,仍可用于委托和治理投票。

注意

归属账户可以同时包含归属中的代币和非归属代币。非归属代币可以立即转移。DelayedVesting、ContinuousVesting、PeriodicVesting 和 PermanentVesting 账户可以在创世之后通过普通消息创建。其他类型的归属账户必须在创世时创建,或作为手动网络升级的一部分创建。当前规范只允许无条件归属(即不存在到达 ET 后代币仍未能完成归属的情况)。

归属账户类型

// VestingAccount defines an interface that any vesting account type must
// implement.
type VestingAccount interface {
    Account

  GetVestedCoins(Time)

Coins
  GetVestingCoins(Time)

Coins

  // TrackDelegation performs internal vesting accounting necessary when
  // delegating from a vesting account. It accepts the current block time, the
  // delegation amount and balance of all coins whose denomination exists in
  // the account's original vesting balance.
  TrackDelegation(Time, Coins, Coins)

  // TrackUndelegation performs internal vesting accounting necessary when a
  // vesting account performs an undelegation.
  TrackUndelegation(Coins)

GetStartTime()

int64
  GetEndTime()

int64
}

BaseVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L11-L35

ContinuousVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L37-L46

DelayedVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L48-L57

Period

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L59-L69
// Stores all vesting periods passed as part of a PeriodicVestingAccount
type Periods []Period

PeriodicVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L71-L81
为了减少临时性的类型检查和断言,并支持账户余额使用方式的灵活性,现有的 x/bank ViewKeeper 接口已更新,包含以下内容:
type ViewKeeper interface {
  // ...

  // Calculates the total locked account balance.
  LockedCoins(ctx sdk.Context, addr sdk.AccAddress)

sdk.Coins

  // Calculates the total spendable balance that can be sent to other accounts.
  SpendableCoins(ctx sdk.Context, addr sdk.AccAddress)

sdk.Coins
}

PermanentLockedAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L83-L94

归属账户规范

对于一个归属账户,我们在后续操作中定义以下概念:
  • OV:原始归属代币数量。它是一个常量值。
  • V:仍处于归属中的 OV 代币数量。它由 OV、StartTime 和 EndTime 推导得出。该值按需计算,而不是按区块计算。
  • V':已归属(已解锁)的 OV 代币数量。该值按需计算,而不是按区块计算。
  • DV:已委托的归属中代币数量。它是一个变量值,直接存储并修改于归属账户中。
  • DF:已委托的已归属(已解锁)代币数量。它是一个变量值,直接存储并修改于归属账户中。
  • BC:OV 代币数量减去任何已转移的代币后的数量(可以为负数,也可以已被委托)。它被视为内嵌基础账户的余额,直接存储并修改于归属账户中。

确定归属中与已归属金额

需要特别注意的是,这些值是按需计算的,而不是在每个区块强制计算(例如在 BeginBlocker 或 EndBlocker 中)。

连续归属账户

要确定给定区块时间 T 下已经归属的代币数量,需要执行以下计算:
  1. 计算 X := T - StartTime
  2. 计算 Y := EndTime - StartTime
  3. 计算 V' := OV * (X / Y)
  4. 计算 V := OV - V'
因此,已归属代币总量为 V',剩余数量 V 为归属中。
func (cva ContinuousVestingAccount)

GetVestedCoins(t Time)

Coins {
    if t <= cva.StartTime {
        // We must handle the case where the start time for a vesting account has
        // been set into the future or when the start of the chain is not exactly
        // known.
        return ZeroCoins
}

else if t >= cva.EndTime {
    return cva.OriginalVesting
}
    x := t - cva.StartTime
    y := cva.EndTime - cva.StartTime

    return cva.OriginalVesting * (x / y)
}

func (cva ContinuousVestingAccount)

GetVestingCoins(t Time)

Coins {
    return cva.OriginalVesting - cva.GetVestedCoins(t)
}

周期性归属账户

周期性归属账户要求在给定区块时间 T 下,计算每个周期内释放的代币。注意,调用 GetVestedCoins 时可能已经过去了多个周期,因此我们必须遍历每个周期,直到该周期结束时间晚于 T 为止。
  1. 设置 CT := StartTime
  2. 设置 V' := 0
对于每个周期 P:
  1. 计算 X := T - CT
  2. 如果 X >= P.Length
    1. 计算 V' += P.Amount
    2. 计算 CT += P.Length
    3. 否则中断
  3. 计算 V := OV - V'
func (pva PeriodicVestingAccount)

GetVestedCoins(t Time)

Coins {
    if t < pva.StartTime {
    return ZeroCoins
}
    ct := pva.StartTime // The start of the vesting schedule
    vested := 0
  periods = pva.GetPeriods()
    for _, period  := range periods {
    if t - ct < period.Length {
    break
}

vested += period.Amount
    ct += period.Length // increment ct to the start of the next vesting period
}

return vested
}

func (pva PeriodicVestingAccount)

GetVestingCoins(t Time)

Coins {
    return pva.OriginalVesting - cva.GetVestedCoins(t)
}

延迟/离散归属账户

延迟归属账户更容易理解,因为在某个时间点之前,全部数量都处于归属中;一旦到达该时间点,所有代币都会变为已归属(已解锁)。这不包括账户最初可能就已解锁的任何代币。
func (dva DelayedVestingAccount)

GetVestedCoins(t Time)

Coins {
    if t >= dva.EndTime {
    return dva.OriginalVesting
}

return ZeroCoins
}

func (dva DelayedVestingAccount)

GetVestingCoins(t Time)

Coins {
    return dva.OriginalVesting - dva.GetVestedCoins(t)
}

转移/发送

在任意给定时间,归属账户可以转移:min((BC + DV) - V, BC)。 换句话说,归属账户可转移的数量,是基础账户余额与“基础账户余额加上当前已委托的归属中代币数量,再减去当前已归属代币数量”两者中的较小值。 不过,考虑到账户余额由 x/bank 模块跟踪,并且我们希望避免加载整个账户余额,因此可以改为确定锁定余额,其定义为 max(V - DV, 0),再由此推导出可支配余额。
func (va VestingAccount)

LockedCoins(t Time)

Coins {
    return max(va.GetVestingCoins(t) - va.DelegatedVesting, 0)
}
然后,x/bank 的 ViewKeeper 可以提供 API,用于确定任意账户的锁定代币和可支配代币:
func (k Keeper)

LockedCoins(ctx Context, addr AccAddress)

Coins {
    acc := k.GetAccount(ctx, addr)
    if acc != nil {
    if acc.IsVesting() {
    return acc.LockedCoins(ctx.BlockTime())
}
 
}

    // non-vesting accounts do not have any locked coins
    return NewCoins()
}

Keepers/Handlers

对应的 x/bank keeper 应根据账户是否为归属账户,正确处理代币发送。
func (k Keeper)

SendCoins(ctx Context, from Account, to Account, amount Coins) {
    bc := k.GetBalances(ctx, from)
    v := k.LockedCoins(ctx, from)
    spendable := bc - v
    newCoins := spendable - amount
    assert(newCoins >= 0)

from.SetBalance(newCoins)

to.AddBalance(amount)

    // save balances...
}

委托

对于尝试委托 D 枚代币的归属账户,需要执行以下操作:
  1. 验证 BC >= D > 0
  2. 计算 X := min(max(V - DV, 0), D)(D 中属于归属中的部分)
  3. 计算 Y := D - X(D 中属于自由代币的部分)
  4. 设置 DV += X
  5. 设置 DF += Y
func (va VestingAccount)

TrackDelegation(t Time, balance Coins, amount Coins) {
    assert(balance <= amount)
    x := min(max(va.GetVestingCoins(t) - va.DelegatedVesting, 0), amount)
    y := amount - x

    va.DelegatedVesting += x
    va.DelegatedFree += y
}
注意 TrackDelegation 只会修改 DelegatedVesting 和 DelegatedFree 字段,因此上游调用方必须通过减去 amount 来修改 Coins 字段。

Keepers/Handlers

func DelegateCoins(t Time, from Account, amount Coins) {
    if isVesting(from) {
    from.TrackDelegation(t, amount)
}

else {
    from.SetBalance(sc - amount)
}

    // save account...
}

解除委托

对于一个尝试解除委托 D 枚代币的归属账户,会执行以下操作:
注意:由于委托/解除委托逻辑中的舍入特性,可能会出现 DV < D 和 (DV + DF) < D 的情况。
  1. 验证 D > 0
  2. 计算 X := min(DF, D)(D 中应变为自由代币的部分,优先使用自由代币)
  3. 计算 Y := min(DV, D - X)(D 中应继续保持为归属代币的部分)
  4. 设置 DF -= X
  5. 设置 DV -= Y
func (cva ContinuousVestingAccount)

TrackUndelegation(amount Coins) {
    x := min(cva.DelegatedFree, amount)
    y := amount - x

    cva.DelegatedFree -= x
    cva.DelegatedVesting -= y
}
注意 TrackUnDelegation 只会修改 DelegatedVesting 和 DelegatedFree 字段,因此上游调用方必须通过加上 amount 来修改 Coins 字段。 注意:如果一笔委托被削减,那么即使该连续归属账户的所有代币都已完成归属,它最终仍可能有多余的 DV 数量。这是因为在解除委托时,会优先返还自由代币。 注意:由于解除委托会对返还的 bond 数量进行截断,解除委托返还(bond refund)数量可能超过已委托的归属(bond)数量;如果被解除委托的代币是非整数,这还可能使验证者的兑换率(tokens/shares)略微上升。

Keeper/Handler

func UndelegateCoins(to Account, amount Coins) {
    if isVesting(to) {
    if to.DelegatedFree + to.DelegatedVesting >= amount {
    to.TrackUndelegation(amount)
            // save account ...
}
 
}

else {
    AddBalance(to, amount)
        // save account...
}
}

Keeper 与 Handler

VestingAccount 的实现位于 x/auth。不过,任何模块中的 keeper(例如 x/staking 中的 staking keeper)如果希望可能使用到归属代币,就必须调用 x/bank keeper 上的显式方法(例如 DelegateCoins),而不是 SendCoins 和 SubtractCoins。 此外,归属账户也应能够花费其从其他用户处收到的任何代币。因此,如果归属账户尝试发送的金额超过其已解锁代币数量,bank 模块的 MsgSend handler 应返回错误。 完整实现细节请参见上述规范。

创世初始化

为了同时初始化归属账户和非归属账户,GenesisAccount 结构体新增了字段:Vesting、StartTime 和 EndTime。本应属于 BaseAccount 或任何非归属类型的账户,其 Vesting = false。创世初始化逻辑(例如 initFromGenesisState)必须根据这些字段正确解析并返回相应的账户类型。
type GenesisAccount struct {
    // ...

    // vesting account fields
    OriginalVesting  sdk.Coins `json:"original_vesting"`
    DelegatedFree    sdk.Coins `json:"delegated_free"`
    DelegatedVesting sdk.Coins `json:"delegated_vesting"`
    StartTime        int64     `json:"start_time"`
    EndTime          int64     `json:"end_time"`
}

func ToAccount(gacc GenesisAccount)

Account {
    bacc := NewBaseAccount(gacc)
    if gacc.OriginalVesting > 0 {
    if ga.StartTime != 0 && ga.EndTime != 0 {
            // return a continuous vesting account
}

else if ga.EndTime != 0 {
            // return a delayed vesting account
}

else {
            // invalid genesis vesting account provided
            panic()
}
 
}

return bacc
}

示例

简单

给定一个拥有 10 枚归属代币的连续归属账户。
OV = 10
DF = 0
DV = 0
BC = 10
V = 10
V' = 0
  1. 立即收到 1 枚代币
    BC = 11
    
  2. 时间流逝,2 枚代币完成归属
    V = 8
    V' = 2
    
  3. 向验证者 A 委托 4 枚代币
    DV = 4
    BC = 7
    
  4. 发送 3 枚代币
    BC = 4
    
  5. 更多时间流逝,又有 2 枚代币完成归属
    V = 6
    V' = 4
    
  6. 发送 2 枚代币。此时,在有更多代币完成归属或收到额外代币之前,该账户不能再发送代币。不过,它仍然可以继续委托。
    BC = 2
    

削减

初始条件与简单示例相同。
  1. 时间流逝,5 枚代币完成归属
    V = 5
    V' = 5
    
  2. 向验证者 A 委托 5 枚代币
    DV = 5
    BC = 5
    
  3. 向验证者 B 委托 5 枚代币
    DF = 5
    BC = 0
    
  4. 验证者 A 被削减 50%,使得对 A 的委托现在只值 2.5 枚代币
  5. 从验证者 A 解除委托(2.5 枚代币)
    DF = 5 - 2.5 = 2.5
    BC = 0 + 2.5 = 2.5
    
  6. 从验证者 B 解除委托(5 枚代币)。此时,该账户只能发送 2.5 枚代币,除非它收到更多代币或有更多代币完成归属。不过,它仍然可以继续委托。
    DV = 5 - 2.5 = 2.5
    DF = 2.5 - 2.5 = 0
    BC = 2.5 + 5 = 7.5
    
    注意此时出现了额外的 DV 数量。

周期性归属

创建一个归属账户,在 1 年内释放 100 枚代币,每个季度归属其中的 1/4。其归属计划如下:
Periods:
- amount: 25stake, length: 7884000
- amount: 25stake, length: 7884000
- amount: 25stake, length: 7884000
- amount: 25stake, length: 7884000
OV = 100
DF = 0
DV = 0
BC = 100
V = 100
V' = 0
  1. 立即收到 1 枚代币
    BC = 101
    
  2. 第 1 个归属周期结束,25 枚代币完成归属
    V = 75
    V' = 25
    
  3. 在第 2 个归属周期内,转入 5 枚代币并委托 5 枚代币
    DV = 5
    BC = 91
    
  4. 第 2 个归属周期结束,25 枚代币完成归属
    V = 50
    V' = 50
    

术语表

  • OriginalVesting:最初属于归属账户的代币数量(按币种分别计算)。这些代币在创世时设定。
  • StartTime:归属账户开始归属时的 BFT 时间。
  • EndTime:归属账户完成全部归属时的 BFT 时间。
  • DelegatedFree:从归属账户委托出去、且在委托时已经完全归属的代币追踪数量(按币种分别计算)。
  • DelegatedVesting:从归属账户委托出去、且在委托时仍处于归属中的代币追踪数量(按币种分别计算)。
  • ContinuousVestingAccount:一种按时间线性归属代币的归属账户实现。
  • DelayedVestingAccount:一种仅在某个给定时间点一次性完成全部代币归属的归属账户实现。
  • PeriodicVestingAccount:一种按照自定义归属计划归属代币的归属账户实现。
  • PermanentLockedAccount:它永远不会释放代币,会将其无限期锁定。即使在锁定期间,该账户中的代币仍可用于委托和治理投票。

CLI

用户可以使用 CLI 查询并与 vesting 模块交互。

交易

tx 命令允许用户与 vesting 模块交互。
simd tx vesting --help

create-periodic-vesting-account

create-periodic-vesting-account 命令会创建一个新的归属账户,并为其分配一笔代币,其中包含一系列代币数量和以秒为单位的周期长度。各周期按顺序执行,也就是说,某个周期的持续时间只有在前一个周期结束后才开始计算。第一个周期的持续时间从账户创建时开始。
simd tx vesting create-periodic-vesting-account [to_address] [periods_json_file] [flags]
示例:
simd tx vesting create-periodic-vesting-account cosmos1.. periods.json

create-vesting-account

create-vesting-account 命令会创建一个新的归属账户,并为其分配一笔代币。该账户可以是延迟归属账户,也可以是连续归属账户,具体由 --delayed 标志决定。所有创建出的归属账户,其开始时间都会设置为区块提交时的时间。必须提供 end\_time,其值为 UNIX 纪元时间戳。
simd tx vesting create-vesting-account [to_address] [amount] [end_time] [flags]
示例:
simd tx vesting create-vesting-account cosmos1.. 100stake 2592000

Intro and Requirements

This specification defines the vesting account implementation that is used by the Cosmos Hub. The requirements for this vesting account is that it should be initialized during genesis with a starting balance X and a vesting end time ET. A vesting account may be initialized with a vesting start time ST and a number of vesting periods P. If a vesting start time is included, the vesting period does not begin until start time is reached. If vesting periods are included, the vesting occurs over the specified number of periods. For all vesting accounts, the owner of the vesting account is able to delegate and undelegate from validators, however they cannot transfer coins to another account until those coins are vested. This specification allows for four different kinds of vesting:
  • Delayed vesting, where all coins are vested once ET is reached.
  • Continuous vesting, where coins begin to vest at ST and vest linearly with respect to time until ET is reached
  • Periodic vesting, where coins begin to vest at ST and vest periodically according to number of periods and the vesting amount per period. The number of periods, length per period, and amount per period are configurable. A periodic vesting account is distinguished from a continuous vesting account in that coins can be released in staggered tranches. For example, a periodic vesting account could be used for vesting arrangements where coins are released quarterly, yearly, or over any other function of tokens over time.
  • Permanent locked vesting, where coins are locked forever. Coins in this account can still be used for delegating and for governance votes even while locked.

Note

Vesting accounts can be initialized with some vesting and non-vesting coins. The non-vesting coins would be immediately transferable. DelayedVesting ContinuousVesting, PeriodicVesting and PermanentVesting accounts can be created with normal messages after genesis. Other types of vesting accounts must be created at genesis, or as part of a manual network upgrade. The current specification only allows for unconditional vesting (ie. there is no possibility of reaching ET and having coins fail to vest).

Vesting Account Types

// VestingAccount defines an interface that any vesting account type must
// implement.
type VestingAccount interface {
    Account

  GetVestedCoins(Time)

Coins
  GetVestingCoins(Time)

Coins

  // TrackDelegation performs internal vesting accounting necessary when
  // delegating from a vesting account. It accepts the current block time, the
  // delegation amount and balance of all coins whose denomination exists in
  // the account's original vesting balance.
  TrackDelegation(Time, Coins, Coins)

  // TrackUndelegation performs internal vesting accounting necessary when a
  // vesting account performs an undelegation.
  TrackUndelegation(Coins)

GetStartTime()

int64
  GetEndTime()

int64
}

BaseVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L11-L35

ContinuousVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L37-L46

DelayedVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L48-L57

Period

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L59-L69
// Stores all vesting periods passed as part of a PeriodicVestingAccount
type Periods []Period

PeriodicVestingAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L71-L81
In order to facilitate less ad-hoc type checking and assertions and to support flexibility in account balance usage, the existing x/bank ViewKeeper interface is updated to contain the following:
type ViewKeeper interface {
  // ...

  // Calculates the total locked account balance.
  LockedCoins(ctx sdk.Context, addr sdk.AccAddress)

sdk.Coins

  // Calculates the total spendable balance that can be sent to other accounts.
  SpendableCoins(ctx sdk.Context, addr sdk.AccAddress)

sdk.Coins
}

PermanentLockedAccount

// Reference: https://github.com/cosmos/cosmos-sdk/tree/release/v0.50.x/proto/cosmos/vesting/v1beta1/vesting.proto#L83-L94

Vesting Account Specification

Given a vesting account, we define the following in the proceeding operations:
  • OV: The original vesting coin amount. It is a constant value.
  • V: The number of OV coins that are still vesting. It is derived by OV, StartTime and EndTime. This value is computed on demand and not on a per-block basis.
  • V': The number of OV coins that are vested (unlocked). This value is computed on demand and not a per-block basis.
  • DV: The number of delegated vesting coins. It is a variable value. It is stored and modified directly in the vesting account.
  • DF: The number of delegated vested (unlocked) coins. It is a variable value. It is stored and modified directly in the vesting account.
  • BC: The number of OV coins less any coins that are transferred (which can be negative or delegated). It is considered to be balance of the embedded base account. It is stored and modified directly in the vesting account.

Determining Vesting & Vested Amounts

It is important to note that these values are computed on demand and not on a mandatory per-block basis (e.g. BeginBlocker or EndBlocker).

Continuously Vesting Accounts

To determine the amount of coins that are vested for a given block time T, the following is performed:
  1. Compute X := T - StartTime
  2. Compute Y := EndTime - StartTime
  3. Compute V' := OV * (X / Y)
  4. Compute V := OV - V'
Thus, the total amount of vested coins is V' and the remaining amount, V, is vesting.
func (cva ContinuousVestingAccount)

GetVestedCoins(t Time)

Coins {
    if t <= cva.StartTime {
        // We must handle the case where the start time for a vesting account has
        // been set into the future or when the start of the chain is not exactly
        // known.
        return ZeroCoins
}

else if t >= cva.EndTime {
    return cva.OriginalVesting
}
    x := t - cva.StartTime
    y := cva.EndTime - cva.StartTime

    return cva.OriginalVesting * (x / y)
}

func (cva ContinuousVestingAccount)

GetVestingCoins(t Time)

Coins {
    return cva.OriginalVesting - cva.GetVestedCoins(t)
}

Periodic Vesting Accounts

Periodic vesting accounts require calculating the coins released during each period for a given block time T. Note that multiple periods could have passed when calling GetVestedCoins, so we must iterate over each period until the end of that period is after T.
  1. Set CT := StartTime
  2. Set V' := 0
For each Period P:
  1. Compute X := T - CT
  2. IF X >= P.Length
    1. Compute V' += P.Amount
    2. Compute CT += P.Length
    3. ELSE break
  3. Compute V := OV - V'
func (pva PeriodicVestingAccount)

GetVestedCoins(t Time)

Coins {
    if t < pva.StartTime {
    return ZeroCoins
}
    ct := pva.StartTime // The start of the vesting schedule
    vested := 0
  periods = pva.GetPeriods()
    for _, period  := range periods {
    if t - ct < period.Length {
    break
}

vested += period.Amount
    ct += period.Length // increment ct to the start of the next vesting period
}

return vested
}

func (pva PeriodicVestingAccount)

GetVestingCoins(t Time)

Coins {
    return pva.OriginalVesting - cva.GetVestedCoins(t)
}

Delayed/Discrete Vesting Accounts

Delayed vesting accounts are easier to reason about as they only have the full amount vesting up until a certain time, then all the coins become vested (unlocked). This does not include any unlocked coins the account may have initially.
func (dva DelayedVestingAccount)

GetVestedCoins(t Time)

Coins {
    if t >= dva.EndTime {
    return dva.OriginalVesting
}

return ZeroCoins
}

func (dva DelayedVestingAccount)

GetVestingCoins(t Time)

Coins {
    return dva.OriginalVesting - dva.GetVestedCoins(t)
}

Transferring/Sending

At any given time, a vesting account may transfer: min((BC + DV) - V, BC). In other words, a vesting account may transfer the minimum of the base account balance and the base account balance plus the number of currently delegated vesting coins less the number of coins vested so far. However, given that account balances are tracked via the x/bank module and that we want to avoid loading the entire account balance, we can instead determine the locked balance, which can be defined as max(V - DV, 0), and infer the spendable balance from that.
func (va VestingAccount)

LockedCoins(t Time)

Coins {
    return max(va.GetVestingCoins(t) - va.DelegatedVesting, 0)
}
The x/bank ViewKeeper can then provide APIs to determine locked and spendable coins for any account:
func (k Keeper)

LockedCoins(ctx Context, addr AccAddress)

Coins {
    acc := k.GetAccount(ctx, addr)
    if acc != nil {
    if acc.IsVesting() {
    return acc.LockedCoins(ctx.BlockTime())
}
 
}

    // non-vesting accounts do not have any locked coins
    return NewCoins()
}

Keepers/Handlers

The corresponding x/bank keeper should appropriately handle sending coins based on if the account is a vesting account or not.
func (k Keeper)

SendCoins(ctx Context, from Account, to Account, amount Coins) {
    bc := k.GetBalances(ctx, from)
    v := k.LockedCoins(ctx, from)
    spendable := bc - v
    newCoins := spendable - amount
    assert(newCoins >= 0)

from.SetBalance(newCoins)

to.AddBalance(amount)

    // save balances...
}

Delegating

For a vesting account attempting to delegate D coins, the following is performed:
  1. Verify BC >= D > 0
  2. Compute X := min(max(V - DV, 0), D) (portion of D that is vesting)
  3. Compute Y := D - X (portion of D that is free)
  4. Set DV += X
  5. Set DF += Y
func (va VestingAccount)

TrackDelegation(t Time, balance Coins, amount Coins) {
    assert(balance <= amount)
    x := min(max(va.GetVestingCoins(t) - va.DelegatedVesting, 0), amount)
    y := amount - x

    va.DelegatedVesting += x
    va.DelegatedFree += y
}
Note TrackDelegation only modifies the DelegatedVesting and DelegatedFree fields, so upstream callers MUST modify the Coins field by subtracting amount.

Keepers/Handlers

func DelegateCoins(t Time, from Account, amount Coins) {
    if isVesting(from) {
    from.TrackDelegation(t, amount)
}

else {
    from.SetBalance(sc - amount)
}

    // save account...
}

Undelegating

For a vesting account attempting to undelegate D coins, the following is performed:
NOTE: DV < D and (DV + DF) < D may be possible due to quirks in the rounding of delegation/undelegation logic.
  1. Verify D > 0
  2. Compute X := min(DF, D) (portion of D that should become free, prioritizing free coins)
  3. Compute Y := min(DV, D - X) (portion of D that should remain vesting)
  4. Set DF -= X
  5. Set DV -= Y
func (cva ContinuousVestingAccount)

TrackUndelegation(amount Coins) {
    x := min(cva.DelegatedFree, amount)
    y := amount - x

    cva.DelegatedFree -= x
    cva.DelegatedVesting -= y
}
Note TrackUnDelegation only modifies the DelegatedVesting and DelegatedFree fields, so upstream callers MUST modify the Coins field by adding amount. Note: If a delegation is slashed, the continuous vesting account ends up with an excess DV amount, even after all its coins have vested. This is because undelegating free coins are prioritized. Note: The undelegation (bond refund) amount may exceed the delegated vesting (bond) amount due to the way undelegation truncates the bond refund, which can increase the validator’s exchange rate (tokens/shares) slightly if the undelegated tokens are non-integral.

Keepers/Handlers

func UndelegateCoins(to Account, amount Coins) {
    if isVesting(to) {
    if to.DelegatedFree + to.DelegatedVesting >= amount {
    to.TrackUndelegation(amount)
            // save account ...
}
 
}

else {
    AddBalance(to, amount)
        // save account...
}
}

Keepers & Handlers

The VestingAccount implementations reside in x/auth. However, any keeper in a module (e.g. staking in x/staking) wishing to potentially utilize any vesting coins, must call explicit methods on the x/bank keeper (e.g. DelegateCoins) opposed to SendCoins and SubtractCoins. In addition, the vesting account should also be able to spend any coins it receives from other users. Thus, the bank module’s MsgSend handler should error if a vesting account is trying to send an amount that exceeds their unlocked coin amount. See the above specification for full implementation details.

Genesis Initialization

To initialize both vesting and non-vesting accounts, the GenesisAccount struct includes new fields: Vesting, StartTime, and EndTime. Accounts meant to be of type BaseAccount or any non-vesting type have Vesting = false. The genesis initialization logic (e.g. initFromGenesisState) must parse and return the correct accounts accordingly based off of these fields.
type GenesisAccount struct {
    // ...

    // vesting account fields
    OriginalVesting  sdk.Coins `json:"original_vesting"`
    DelegatedFree    sdk.Coins `json:"delegated_free"`
    DelegatedVesting sdk.Coins `json:"delegated_vesting"`
    StartTime        int64     `json:"start_time"`
    EndTime          int64     `json:"end_time"`
}

func ToAccount(gacc GenesisAccount)

Account {
    bacc := NewBaseAccount(gacc)
    if gacc.OriginalVesting > 0 {
    if ga.StartTime != 0 && ga.EndTime != 0 {
            // return a continuous vesting account
}

else if ga.EndTime != 0 {
            // return a delayed vesting account
}

else {
            // invalid genesis vesting account provided
            panic()
}
 
}

return bacc
}

Examples

Simple

Given a continuous vesting account with 10 vesting coins.
OV = 10
DF = 0
DV = 0
BC = 10
V = 10
V' = 0
  1. Immediately receives 1 coin
    BC = 11
    
  2. Time passes, 2 coins vest
    V = 8
    V' = 2
    
  3. Delegates 4 coins to validator A
    DV = 4
    BC = 7
    
  4. Sends 3 coins
    BC = 4
    
  5. More time passes, 2 more coins vest
    V = 6
    V' = 4
    
  6. Sends 2 coins. At this point the account cannot send anymore until further coins vest or it receives additional coins. It can still however, delegate.
    BC = 2
    

Slashing

Same initial starting conditions as the simple example.
  1. Time passes, 5 coins vest
    V = 5
    V' = 5
    
  2. Delegate 5 coins to validator A
    DV = 5
    BC = 5
    
  3. Delegate 5 coins to validator B
    DF = 5
    BC = 0
    
  4. Validator A gets slashed by 50%, making the delegation to A now worth 2.5 coins
  5. Undelegate from validator A (2.5 coins)
    DF = 5 - 2.5 = 2.5
    BC = 0 + 2.5 = 2.5
    
  6. Undelegate from validator B (5 coins). The account at this point can only send 2.5 coins unless it receives more coins or until more coins vest. It can still however, delegate.
    DV = 5 - 2.5 = 2.5
    DF = 2.5 - 2.5 = 0
    BC = 2.5 + 5 = 7.5
    
    Notice how we have an excess amount of DV.

Periodic Vesting

A vesting account is created where 100 tokens will be released over 1 year, with 1/4 of tokens vesting each quarter. The vesting schedule would be as follows:
Periods:
- amount: 25stake, length: 7884000
- amount: 25stake, length: 7884000
- amount: 25stake, length: 7884000
- amount: 25stake, length: 7884000
OV = 100
DF = 0
DV = 0
BC = 100
V = 100
V' = 0
  1. Immediately receives 1 coin
    BC = 101
    
  2. Vesting period 1 passes, 25 coins vest
    V = 75
    V' = 25
    
  3. During vesting period 2, 5 coins are transferred and 5 coins are delegated
    DV = 5
    BC = 91
    
  4. Vesting period 2 passes, 25 coins vest
    V = 50
    V' = 50
    

Glossary

  • OriginalVesting: The amount of coins (per denomination) that are initially part of a vesting account. These coins are set at genesis.
  • StartTime: The BFT time at which a vesting account starts to vest.
  • EndTime: The BFT time at which a vesting account is fully vested.
  • DelegatedFree: The tracked amount of coins (per denomination) that are delegated from a vesting account that have been fully vested at time of delegation.
  • DelegatedVesting: The tracked amount of coins (per denomination) that are delegated from a vesting account that were vesting at time of delegation.
  • ContinuousVestingAccount: A vesting account implementation that vests coins linearly over time.
  • DelayedVestingAccount: A vesting account implementation that only fully vests all coins at a given time.
  • PeriodicVestingAccount: A vesting account implementation that vests coins according to a custom vesting schedule.
  • PermanentLockedAccount: It does not ever release coins, locking them indefinitely. Coins in this account can still be used for delegating and for governance votes even while locked.

CLI

A user can query and interact with the vesting module using the CLI.

Transactions

The tx commands allow users to interact with the vesting module.
simd tx vesting --help

create-periodic-vesting-account

The create-periodic-vesting-account command creates a new vesting account funded with an allocation of tokens, where a sequence of coins and period length in seconds. Periods are sequential, in that the duration of a period only starts at the end of the previous period. The duration of the first period starts upon account creation.
simd tx vesting create-periodic-vesting-account [to_address] [periods_json_file] [flags]
Example:
simd tx vesting create-periodic-vesting-account cosmos1.. periods.json

create-vesting-account

The create-vesting-account command creates a new vesting account funded with an allocation of tokens. The account can either be a delayed or continuous vesting account, which is determined by the ‘—delayed’ flag. All vesting accouts created will have their start time set by the committed block’s time. The end_time must be provided as a UNIX epoch timestamp.
simd tx vesting create-vesting-account [to_address] [amount] [end_time] [flags]
Example:
simd tx vesting create-vesting-account cosmos1.. 100stake 2592000