diff --git a/docs/spec/vesting/01_state.md b/docs/spec/vesting/01_state.md new file mode 100644 index 000000000000..165fad57d119 --- /dev/null +++ b/docs/spec/vesting/01_state.md @@ -0,0 +1,456 @@ + + +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 `T`. A vesting account may be initialized with a vesting start time `T'` and a number of vesting periods `P`. If a vesting start time is included, the vesting period will not begin until start time is reached. If vesting periods is included, the vesting will occur 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 three different kinds of vesting: +* Delayed vesting, where all coins are vested once `T` is reached. +* Continous vesting, where coins begin to vest at `T'` and vest linearly with respect to time until `T` is reached +* Periodic vesting, where coins begin to vest at `T'` 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. + +## Note +The current specification does not allow for vesting accounts to be created with normal messages after genesis. All 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 `T` and having coins fail to vest). + + +## Vesting Account Types + + +```go +// VestingAccount defines an interface that any vesting account type must +// implement. +type VestingAccount interface { + Account + + GetVestedCoins(Time) Coins + GetVestingCoins(Time) Coins + + // Delegation and undelegation accounting that returns the resulting base + // coins amount. + TrackDelegation(Time, Coins) + TrackUndelegation(Coins) + + GetStartTime() int64 + GetEndTime() int64 +} + +// BaseVestingAccount implements the VestingAccount interface. It contains all +// the necessary fields needed for any vesting account implementation. +type BaseVestingAccount struct { + BaseAccount + + OriginalVesting Coins // coins in account upon initialization + DelegatedFree Coins // coins that are vested and delegated + DelegatedVesting Coins // coins that vesting and delegated + + EndTime int64 // when the coins become unlocked +} + +// ContinuousVestingAccount implements the VestingAccount interface. It +// continuously vests by unlocking coins linearly with respect to time. +type ContinuousVestingAccount struct { + BaseVestingAccount + + StartTime int64 // when the coins start to vest +} + +// DelayedVestingAccount implements the VestingAccount interface. It vests all +// coins after a specific time, but non prior. In other words, it keeps them +// locked until a specified time. +type DelayedVestingAccount struct { + BaseVestingAccount +} + +// VestingPeriod defines a length of time and amount of coins that will vest +type VestingPeriod struct { + PeriodLength int64 // length of the period, in seconds + VestingAmount Coins // amount of coins vesting during this period +} + +// Stores all vesting periods passed as part of a PeriodicVestingAccount +type VestingPeriods []VestingPeriod + +type PeriodicVestingAccount struct { + ContinuousVestingAccount + VestingPeriods VestingPeriods // the vesting schedule +} +``` + +## 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_. + +```go +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 calculating GetVestedCoins, so we must iterate over each period until the end of that period is after `T`. + +Set `CT := StartTime` +Set `V' := 0` +For each Period P: + 1. Compute `X := T - CT` + 2. IF `X >= P.Length` + a. Compute `V' += P.Amount` + b. Compute `CT += P.Length` + ELSE break + 3. Compute `V := OV = V'` + +```go +func (pva PeriodicVestingAccount) GetVestedCoins(t Time) Coins { + if t < pva.StartTime { + return ZeroCoins + } + ct := pva.StartTime // The time of the current period start + vested := 0 + periods = pva.GetPeriods() + for i := 0; i < len(periods); i++ { + x := t - ct + if x>= periods[i].Length { + vested += periods[i].Amount + ct += periods[i].Length + } else {break} + } + 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. + +```go +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. + +```go +func (va VestingAccount) SpendableCoins(t Time) Coins { + bc := va.GetCoins() + return min((bc + va.DelegatedVesting) - va.GetVestingCoins(t), bc) +} +``` + +#### Keepers/Handlers + +The corresponding `x/bank` keeper should appropriately handle sending coins +based on if the account is a vesting account or not. + +```go +func SendCoins(t Time, from Account, to Account, amount Coins) { + bc := from.GetCoins() + + if isVesting(from) { + sc := from.SpendableCoins(t) + assert(amount <= sc) + } + + newCoins := bc - amount + assert(newCoins >= 0) + + from.SetCoins(bc - amount) + to.SetCoins(amount) + + // save accounts... +} +``` + +### 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, prioritizing vesting coins) +3. Compute `Y := D - X` (portion of `D` that is free) +4. Set `DV += X` +5. Set `DF += Y` +6. Set `BC -= D` + +```go +func (va VestingAccount) TrackDelegation(t Time, amount Coins) { + x := min(max(va.GetVestingCoins(t) - va.DelegatedVesting, 0), amount) + y := amount - x + + va.DelegatedVesting += x + va.DelegatedFree += y + va.SetCoins(va.GetCoins() - amount) +} +``` + +#### Keepers/Handlers + +```go +func DelegateCoins(t Time, from Account, amount Coins) { + bc := from.GetCoins() + assert(amount <= bc) + + if isVesting(from) { + from.TrackDelegation(t, amount) + } else { + from.SetCoins(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` +6. Set `BC += D` + +```go +func (cva ContinuousVestingAccount) TrackUndelegation(amount Coins) { + x := min(cva.DelegatedFree, amount) + y := amount - x + + cva.DelegatedFree -= x + cva.DelegatedVesting -= y + cva.SetCoins(cva.GetCoins() + amount) +} +``` + +**Note**: If a delegation is slashed, the continuous vesting account will end 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 + +```go +func UndelegateCoins(to Account, amount Coins) { + if isVesting(to) { + if to.DelegatedFree + to.DelegatedVesting >= amount { + to.TrackUndelegation(amount) + // save account ... + } + } else { + AddCoins(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` includes the +following fields: `Vesting`, `StartTime`, and `EndTime`. Accounts meant to be +of type `BaseAccount` or any non-vesting type will have `OriginalVesting = 0`. The +genesis initialization logic parses the account fields and returns the correct accounts +accordinglyz. + +```go +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"` + VestingPeriods VestingPeriods `json:"vesting_periods"` +} + +func ToAccount(gacc GenesisAccount) Account { + bacc := NewBaseAccount(gacc) + + if gacc.OriginalVesting > 0 { + if len(ga.VestingPeriods) > 0 { + // return a periodic vesting account + } + 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 + ```text + BC = 11 + ``` +2. Time passes, 2 coins vest + ```text + V = 8 + V' = 2 + ``` +3. Delegates 4 coins to validator A + ```text + DV = 4 + BC = 7 + ``` +4. Sends 3 coins + ```text + BC = 4 + ``` +5. More time passes, 2 more coins vest + ```text + 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. + ```text + BC = 2 + ``` + +### Slashing + +Same initial starting conditions as the simple example. + +1. Time passes, 5 coins vest + ```text + V = 5 + V' = 5 + ``` +2. Delegate 5 coins to validator A + ```text + DV = 5 + BC = 5 + ``` +3. Delegate 5 coins to validator B + ```text + 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) + ```text + 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. + ```text + 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`. + +## 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. \ No newline at end of file diff --git a/docs/spec/vesting/02_messages.md b/docs/spec/vesting/02_messages.md new file mode 100644 index 000000000000..88c49d85f308 --- /dev/null +++ b/docs/spec/vesting/02_messages.md @@ -0,0 +1,3 @@ +# Messages + +The vesting module currently has no messages. All accounts must be created as part of a genesis event. \ No newline at end of file diff --git a/x/auth/alias.go b/x/auth/alias.go index f6a721497ccc..ac1487dcf31a 100644 --- a/x/auth/alias.go +++ b/x/auth/alias.go @@ -29,45 +29,35 @@ const ( var ( // functions aliases - NewAnteHandler = ante.NewAnteHandler - GetSignerAcc = ante.GetSignerAcc - ValidateSigCount = ante.ValidateSigCount - ValidateMemo = ante.ValidateMemo - ProcessPubKey = ante.ProcessPubKey - DefaultSigVerificationGasConsumer = ante.DefaultSigVerificationGasConsumer - DeductFees = ante.DeductFees - EnsureSufficientMempoolFees = ante.EnsureSufficientMempoolFees - SetGasMeter = ante.SetGasMeter - GetSignBytes = ante.GetSignBytes - NewAccountKeeper = keeper.NewAccountKeeper - NewQuerier = keeper.NewQuerier - NewBaseAccount = types.NewBaseAccount - ProtoBaseAccount = types.ProtoBaseAccount - NewBaseAccountWithAddress = types.NewBaseAccountWithAddress - NewBaseVestingAccount = types.NewBaseVestingAccount - NewContinuousVestingAccountRaw = types.NewContinuousVestingAccountRaw - NewContinuousVestingAccount = types.NewContinuousVestingAccount - NewDelayedVestingAccountRaw = types.NewDelayedVestingAccountRaw - NewDelayedVestingAccount = types.NewDelayedVestingAccount - NewAccountRetriever = types.NewAccountRetriever - RegisterCodec = types.RegisterCodec - NewGenesisState = types.NewGenesisState - DefaultGenesisState = types.DefaultGenesisState - ValidateGenesis = types.ValidateGenesis - AddressStoreKey = types.AddressStoreKey - NewParams = types.NewParams - ParamKeyTable = types.ParamKeyTable - DefaultParams = types.DefaultParams - NewQueryAccountParams = types.NewQueryAccountParams - NewStdTx = types.NewStdTx - CountSubKeys = types.CountSubKeys - NewStdFee = types.NewStdFee - StdSignBytes = types.StdSignBytes - DefaultTxDecoder = types.DefaultTxDecoder - DefaultTxEncoder = types.DefaultTxEncoder - NewTxBuilder = types.NewTxBuilder - NewTxBuilderFromCLI = types.NewTxBuilderFromCLI - MakeSignature = types.MakeSignature + NewBaseAccount = types.NewBaseAccount + ProtoBaseAccount = types.ProtoBaseAccount + NewBaseAccountWithAddress = types.NewBaseAccountWithAddress + NewBaseVestingAccount = types.NewBaseVestingAccount + NewContinuousVestingAccountRaw = types.NewContinuousVestingAccountRaw + NewContinuousVestingAccount = types.NewContinuousVestingAccount + NewDelayedVestingAccountRaw = types.NewDelayedVestingAccountRaw + NewDelayedVestingAccount = types.NewDelayedVestingAccount + NewPeriodicVestingAccountRaw = types.NewPeriodicVestingAccountRaw + NewPeriodicVestingAccount = types.NewPeriodicVestingAccount + RegisterCodec = types.RegisterCodec + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + ValidateGenesis = types.ValidateGenesis + AddressStoreKey = types.AddressStoreKey + NewParams = types.NewParams + ParamKeyTable = types.ParamKeyTable + DefaultParams = types.DefaultParams + NewQueryAccountParams = types.NewQueryAccountParams + NewStdTx = types.NewStdTx + CountSubKeys = types.CountSubKeys + NewStdFee = types.NewStdFee + StdSignBytes = types.StdSignBytes + DefaultTxDecoder = types.DefaultTxDecoder + DefaultTxEncoder = types.DefaultTxEncoder + NewTxBuilder = types.NewTxBuilder + NewTxBuilderFromCLI = types.NewTxBuilderFromCLI + MakeSignature = types.MakeSignature + NewAccountRetriever = types.NewAccountRetriever // variable aliases ModuleCdc = types.ModuleCdc @@ -81,23 +71,20 @@ var ( ) type ( - SignatureVerificationGasConsumer = ante.SignatureVerificationGasConsumer - Account = exported.Account - VestingAccount = exported.VestingAccount - AccountKeeper = keeper.AccountKeeper - BaseAccount = types.BaseAccount - BaseVestingAccount = types.BaseVestingAccount - ContinuousVestingAccount = types.ContinuousVestingAccount - DelayedVestingAccount = types.DelayedVestingAccount - NodeQuerier = types.NodeQuerier - AccountRetriever = types.AccountRetriever - GenesisState = types.GenesisState - Params = types.Params - QueryAccountParams = types.QueryAccountParams - StdSignMsg = types.StdSignMsg - StdTx = types.StdTx - StdFee = types.StdFee - StdSignDoc = types.StdSignDoc - StdSignature = types.StdSignature - TxBuilder = types.TxBuilder + Account = exported.Account + VestingAccount = exported.VestingAccount + BaseAccount = types.BaseAccount + BaseVestingAccount = types.BaseVestingAccount + ContinuousVestingAccount = types.ContinuousVestingAccount + DelayedVestingAccount = types.DelayedVestingAccount + PeriodicVestingAccount = types.PeriodicVestingAccount + GenesisState = types.GenesisState + Params = types.Params + QueryAccountParams = types.QueryAccountParams + StdSignMsg = types.StdSignMsg + StdTx = types.StdTx + StdFee = types.StdFee + StdSignDoc = types.StdSignDoc + StdSignature = types.StdSignature + TxBuilder = types.TxBuilder ) diff --git a/x/auth/exported/exported.go b/x/auth/exported/exported.go index e78cd1ece177..cae0d8c27b1f 100644 --- a/x/auth/exported/exported.go +++ b/x/auth/exported/exported.go @@ -2,6 +2,8 @@ package exported import ( "time" + "fmt" + "strings" "github.com/tendermint/tendermint/crypto" @@ -56,4 +58,31 @@ type VestingAccount interface { GetOriginalVesting() sdk.Coins GetDelegatedFree() sdk.Coins GetDelegatedVesting() sdk.Coins + + GetVestingPeriods() VestingPeriods +} + +// VestingPeriod defines a length of time and amount of coins that will vest +type VestingPeriod struct { + PeriodLength int64 // length of the period, in seconds + VestingAmount sdk.Coins // amount of coins vesting during this period } + +// VestingPeriods stores all vesting periods passed as part of a PeriodicVestingAccount +type VestingPeriods []VestingPeriod + +// String VestingPeriod implements stringer interface +func (p VestingPeriod) String() string { + return fmt.Sprintf(`Period Length: %d + VestingAmount: %s`, p.PeriodLength, p.VestingAmount) +} + +// String VestingPeriods implements stringer interface +func (vp VestingPeriods) String() string { + var periodsListString []string + for _, period := range vp { + periodsListString = append(periodsListString, period.String()) + } + return strings.TrimSpace(fmt.Sprintf(`Vesting Periods: + %s`, strings.Join(periodsListString, ", "))) +} \ No newline at end of file diff --git a/x/auth/types/account.go b/x/auth/types/account.go index 31a77b15c86b..b531215aff35 100644 --- a/x/auth/types/account.go +++ b/x/auth/types/account.go @@ -467,6 +467,160 @@ func (cva *ContinuousVestingAccount) GetEndTime() int64 { return cva.EndTime } +// GetVestingPeriods returns empty since a continuous vesting account has no vesting periods. +func (cva *ContinuousVestingAccount) GetVestingPeriods() exported.VestingPeriods { + return exported.VestingPeriods{} +} + +//----------------------------------------------------------------------------- +// Continuous Vesting Account + +var _ exported.VestingAccount = (*PeriodicVestingAccount)(nil) + +// PeriodicVestingAccount implements the VestingAccount interface. It +// periodically vests by unlocking coins during each specified period +type PeriodicVestingAccount struct { + *ContinuousVestingAccount + VestingPeriods exported.VestingPeriods // the vesting schedule +} + +// NewPeriodicVestingAccountRaw creates a new PeriodicVestingAccount object from BaseVestingAccount +func NewPeriodicVestingAccountRaw(bva *BaseVestingAccount, + startTime int64, periods exported.VestingPeriods) *PeriodicVestingAccount { + + cva := &ContinuousVestingAccount{ + StartTime: startTime, + BaseVestingAccount: bva, + } + return &PeriodicVestingAccount{ + ContinuousVestingAccount: cva, + VestingPeriods: periods, + } +} + +// NewPeriodicVestingAccount returns a new PeriodicVestingAccount +func NewPeriodicVestingAccount( + baseAcc *BaseAccount, StartTime int64, periods exported.VestingPeriods) *PeriodicVestingAccount { + + endTime := StartTime + for _, p := range periods { + endTime += p.PeriodLength + } + baseVestingAcc := &BaseVestingAccount{ + BaseAccount: baseAcc, + OriginalVesting: baseAcc.Coins, + EndTime: endTime, + } + + cva := &ContinuousVestingAccount{ + StartTime: StartTime, + BaseVestingAccount: baseVestingAcc, + } + + return &PeriodicVestingAccount{ + ContinuousVestingAccount: cva, + VestingPeriods: periods, + } +} + +func (pva PeriodicVestingAccount) String() string { + var pubkey string + + if pva.PubKey != nil { + pubkey = sdk.MustBech32ifyAccPub(pva.PubKey) + } + + return fmt.Sprintf(`Periodic Vesting Account: + Address: %s + Pubkey: %s + Coins: %s + AccountNumber: %d + Sequence: %d + OriginalVesting: %s + DelegatedFree: %s + DelegatedVesting: %s + StartTime: %d + End Time: %d + VestingPeriods: %s `, + pva.Address, pubkey, pva.Coins, pva.AccountNumber, pva.Sequence, + pva.OriginalVesting, pva.DelegatedFree, pva.DelegatedVesting, + pva.StartTime, pva.EndTime, pva.VestingPeriods, + ) +} + +// GetVestedCoins returns the total number of vested coins. If no coins are vested, +// nil is returned. +func (pva PeriodicVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins { + var vestedCoins sdk.Coins + + // 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. + if blockTime.Unix() <= pva.StartTime { + return vestedCoins + } else if blockTime.Unix() >= pva.EndTime { + return pva.OriginalVesting + } + + // track the start time of the next period + currentPeriodStartTime := pva.StartTime + numberPeriods := len(pva.VestingPeriods) + for i := 0; i < numberPeriods; i++ { + x := blockTime.Unix() - currentPeriodStartTime + // fmt.Printf(`Current Period: %v + // Current Period Start Time: %v + // Current value of x: %v + // Current Period Length: %v + // `, i, currentPeriodStartTime, x, pva.VestingPeriods[i].PeriodLength) + if x >= pva.VestingPeriods[i].PeriodLength { + // fmt.Printf(` + // Adding some coins for period %v + // `, i) + vestedCoins = vestedCoins.Add(pva.VestingPeriods[i].VestingAmount) + // Update the start time of the next period + currentPeriodStartTime += pva.VestingPeriods[i].PeriodLength + } else { + break + } + } + return vestedCoins +} + +// GetVestingCoins returns the total number of vesting coins. If no coins are +// vesting, nil is returned. +func (pva PeriodicVestingAccount) GetVestingCoins(blockTime time.Time) sdk.Coins { + return pva.OriginalVesting.Sub(pva.GetVestedCoins(blockTime)) +} + +// SpendableCoins returns the total number of spendable coins per denom for a +// periodic vesting account. +func (pva PeriodicVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins { + return pva.spendableCoins(pva.GetVestingCoins(blockTime)) +} + +// TrackDelegation tracks a desired delegation amount by setting the appropriate +// values for the amount of delegated vesting, delegated free, and reducing the +// overall amount of base coins. +func (pva *PeriodicVestingAccount) TrackDelegation(blockTime time.Time, amount sdk.Coins) { + pva.trackDelegation(pva.GetVestingCoins(blockTime), amount) +} + +// GetStartTime returns the time when vesting starts for a periodic vesting +// account. +func (pva *PeriodicVestingAccount) GetStartTime() int64 { + return pva.StartTime +} + +// GetEndTime returns the time when vesting ends for a periodic vesting account. +func (pva *PeriodicVestingAccount) GetEndTime() int64 { + return pva.EndTime +} + +// GetVestingPeriods returns vesting periods associated with periodic vesting account. +func (pva *PeriodicVestingAccount) GetVestingPeriods() exported.VestingPeriods { + return pva.VestingPeriods +} + //----------------------------------------------------------------------------- // Delayed Vesting Account @@ -535,3 +689,8 @@ func (dva *DelayedVestingAccount) GetStartTime() int64 { func (dva *DelayedVestingAccount) GetEndTime() int64 { return dva.EndTime } + +// GetVestingPeriods returns empty since a continuous vesting account has no vesting periods. +func (dva *DelayedVestingAccount) GetVestingPeriods() exported.VestingPeriods { + return exported.VestingPeriods{} +} diff --git a/x/auth/types/account_retriever_test.go b/x/auth/types/account_retriever_test.go index 76126db115d3..8d2d4e0462d3 100644 --- a/x/auth/types/account_retriever_test.go +++ b/x/auth/types/account_retriever_test.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/tests/mocks" ) -var errFoo = errors.New("dummy") +var errDummy = errors.New("dummy") func TestAccountRetriever(t *testing.T) { mockCtrl := gomock.NewController(t) @@ -23,18 +23,18 @@ func TestAccountRetriever(t *testing.T) { require.NoError(t, err) mockNodeQuerier.EXPECT().QueryWithData(gomock.Eq("custom/acc/account"), - gomock.Eq(bs)).Return(nil, int64(0), errFoo).Times(1) + gomock.Eq(bs)).Return(nil, int64(0), errDummy).Times(1) _, err = accRetr.GetAccount(addr) require.Error(t, err) mockNodeQuerier.EXPECT().QueryWithData(gomock.Eq("custom/acc/account"), - gomock.Eq(bs)).Return(nil, int64(0), errFoo).Times(1) + gomock.Eq(bs)).Return(nil, int64(0), errDummy).Times(1) n, s, err := accRetr.GetAccountNumberSequence(addr) require.Error(t, err) require.Equal(t, uint64(0), n) require.Equal(t, uint64(0), s) mockNodeQuerier.EXPECT().QueryWithData(gomock.Eq("custom/acc/account"), - gomock.Eq(bs)).Return(nil, int64(0), errFoo).Times(1) + gomock.Eq(bs)).Return(nil, int64(0), errDummy).Times(1) require.Error(t, accRetr.EnsureExists(addr)) } diff --git a/x/auth/types/account_test.go b/x/auth/types/account_test.go index 1ea24f470db4..3c8d38d4cb99 100644 --- a/x/auth/types/account_test.go +++ b/x/auth/types/account_test.go @@ -9,6 +9,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/exported" ) var ( @@ -479,3 +480,249 @@ func TestTrackUndelegationDelVestingAcc(t *testing.T) { require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, dva.DelegatedVesting) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 75)}, dva.GetCoins()) } + +func TestGetVestedCoinsPeriodicVestingAcc(t *testing.T) { + now := tmtime.Now() + endTime := now.Add(24 * time.Hour) + periods := exported.VestingPeriods{ + exported.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + _, _, addr := KeyTestPubAddr() + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := NewBaseAccountWithAddress(addr) + bacc.SetCoins(origCoins) + pva := NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + + // require no coins vested at the beginning of the vesting schedule + vestedCoins := pva.GetVestedCoins(now) + require.Nil(t, vestedCoins) + + // require all coins vested at the end of the vesting schedule + vestedCoins = pva.GetVestedCoins(endTime) + require.Equal(t, origCoins, vestedCoins) + + // require no coins vested during first vesting period + vestedCoins = pva.GetVestedCoins(now.Add(6 * time.Hour)) + require.Nil(t, vestedCoins) + + // require 50% of coins vested after period 1 + vestedCoins = pva.GetVestedCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) + + // require period 2 coins don't vest until period is over + vestedCoins = pva.GetVestedCoins(now.Add(15 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) + + // require 75% of coins vested after period 2 + vestedCoins = pva.GetVestedCoins(now.Add(18 * time.Hour)) + require.Equal(t, + sdk.Coins{ + sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)}, vestedCoins) + + // require 100% of coins vested + vestedCoins = pva.GetVestedCoins(now.Add(48 * time.Hour)) + require.Equal(t, origCoins, vestedCoins) +} + +func TestGetVestingCoinsPeriodicVestingAcc(t *testing.T) { + now := tmtime.Now() + endTime := now.Add(24 * time.Hour) + periods := exported.VestingPeriods{ + exported.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + _, _, addr := KeyTestPubAddr() + origCoins := sdk.Coins{ + sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := NewBaseAccountWithAddress(addr) + bacc.SetCoins(origCoins) + pva := NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + + // require all coins vesting at the beginning of the vesting schedule + vestingCoins := pva.GetVestingCoins(now) + require.Equal(t, origCoins, vestingCoins) + + // require no coins vesting at the end of the vesting schedule + vestingCoins = pva.GetVestingCoins(endTime) + require.Nil(t, vestingCoins) + + // require 50% of coins vesting + vestingCoins = pva.GetVestingCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) + + // require 50% of coins vesting after period 1, but before period 2 completes. + vestingCoins = pva.GetVestingCoins(now.Add(15 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) + + // require 25% of coins vesting after period 2 + vestingCoins = pva.GetVestingCoins(now.Add(18 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins) + + // require 0% of coins vesting after vesting complete + vestingCoins = pva.GetVestingCoins(now.Add(48 * time.Hour)) + require.Nil(t, vestingCoins) +} + +func TestSpendableCoinsPeriodicVestingAcc(t *testing.T) { + now := tmtime.Now() + endTime := now.Add(24 * time.Hour) + periods := exported.VestingPeriods{ + exported.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + _, _, addr := KeyTestPubAddr() + origCoins := sdk.Coins{ + sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := NewBaseAccountWithAddress(addr) + bacc.SetCoins(origCoins) + pva := NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + + // require that there exist no spendable coins at the beginning of the + // vesting schedule + spendableCoins := pva.SpendableCoins(now) + require.Nil(t, spendableCoins) + + // require that all original coins are spendable at the end of the vesting + // schedule + spendableCoins = pva.SpendableCoins(endTime) + require.Equal(t, origCoins, spendableCoins) + + // require that all vested coins (50%) are spendable + spendableCoins = pva.SpendableCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, spendableCoins) + + // receive some coins + recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)} + pva.SetCoins(pva.GetCoins().Add(recvAmt)) + + // require that all vested coins (50%) are spendable plus any received + spendableCoins = pva.SpendableCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 100)}, spendableCoins) + + // spend all spendable coins + pva.SetCoins(pva.GetCoins().Sub(spendableCoins)) + + // require that no more coins are spendable + spendableCoins = pva.SpendableCoins(now.Add(12 * time.Hour)) + require.Nil(t, spendableCoins) +} + +func TestTrackDelegationPeriodicVestingAcc(t *testing.T) { + now := tmtime.Now() + endTime := now.Add(24 * time.Hour) + periods := exported.VestingPeriods{ + exported.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + _, _, addr := KeyTestPubAddr() + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := NewBaseAccountWithAddress(addr) + bacc.SetCoins(origCoins) + + // require the ability to delegate all vesting coins + pva := NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + pva.TrackDelegation(now, origCoins) + require.Equal(t, origCoins, pva.DelegatedVesting) + require.Nil(t, pva.DelegatedFree) + require.Nil(t, pva.GetCoins()) + + // require the ability to delegate all vested coins + bacc.SetCoins(origCoins) + pva = NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + pva.TrackDelegation(endTime, origCoins) + require.Nil(t, pva.DelegatedVesting) + require.Equal(t, origCoins, pva.DelegatedFree) + require.Nil(t, pva.GetCoins()) + + // require the ability to delegate all vesting coins (50%) and all vested coins (50%) + bacc.SetCoins(origCoins) + pva = NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + pva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedVesting) + require.Nil(t, pva.DelegatedFree) + + pva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedVesting) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedFree) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000)}, pva.GetCoins()) + + // require no modifications when delegation amount is zero or not enough funds + bacc.SetCoins(origCoins) + pva = NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + require.Panics(t, func() { + pva.TrackDelegation(endTime, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)}) + }) + require.Nil(t, pva.DelegatedVesting) + require.Nil(t, pva.DelegatedFree) + require.Equal(t, origCoins, pva.GetCoins()) +} + +func TestTrackUndelegationPeriodicVestingAcc(t *testing.T) { + now := tmtime.Now() + endTime := now.Add(24 * time.Hour) + periods := exported.VestingPeriods{ + exported.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + exported.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + _, _, addr := KeyTestPubAddr() + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := NewBaseAccountWithAddress(addr) + bacc.SetCoins(origCoins) + + // require the ability to undelegate all vesting coins + pva := NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + pva.TrackDelegation(now, origCoins) + pva.TrackUndelegation(origCoins) + require.Nil(t, pva.DelegatedFree) + require.Nil(t, pva.DelegatedVesting) + require.Equal(t, origCoins, pva.GetCoins()) + + // require the ability to undelegate all vested coins + bacc.SetCoins(origCoins) + pva = NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + + pva.TrackDelegation(endTime, origCoins) + pva.TrackUndelegation(origCoins) + require.Nil(t, pva.DelegatedFree) + require.Nil(t, pva.DelegatedVesting) + require.Equal(t, origCoins, pva.GetCoins()) + + // require no modifications when the undelegation amount is zero + bacc.SetCoins(origCoins) + pva = NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + + require.Panics(t, func() { + pva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)}) + }) + require.Nil(t, pva.DelegatedFree) + require.Nil(t, pva.DelegatedVesting) + require.Equal(t, origCoins, pva.GetCoins()) + + // vest 50% and delegate to two validators + pva = NewPeriodicVestingAccount(&bacc, now.Unix(), periods) + pva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + pva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + + // undelegate from one validator that got slashed 50% + pva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, pva.DelegatedFree) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, pva.DelegatedVesting) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 25)}, pva.GetCoins()) + + // undelegate from the other validator that did not get slashed + pva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + require.Nil(t, pva.DelegatedFree) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, pva.DelegatedVesting) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 75)}, pva.GetCoins()) +} diff --git a/x/auth/types/codec.go b/x/auth/types/codec.go index 81a6ee894486..873cc2173938 100644 --- a/x/auth/types/codec.go +++ b/x/auth/types/codec.go @@ -16,7 +16,7 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(StdTx{}, "cosmos-sdk/StdTx", nil) } -// module wide codec +// ModuleCdc module wide codec var ModuleCdc *codec.Codec func init() { diff --git a/x/auth/types/keys.go b/x/auth/types/keys.go index 149fe2ba51f3..4e40fce0c0d4 100644 --- a/x/auth/types/keys.go +++ b/x/auth/types/keys.go @@ -5,7 +5,7 @@ import ( ) const ( - // module name + // ModuleName name used throughout the module ModuleName = "auth" // StoreKey is string representation of the store key for auth @@ -22,7 +22,7 @@ var ( // AddressStoreKeyPrefix prefix for account-by-address store AddressStoreKeyPrefix = []byte{0x01} - // param key for global account number + // GlobalAccountNumberKey param key for global account number GlobalAccountNumberKey = []byte("globalAccountNumber") ) diff --git a/x/auth/types/params.go b/x/auth/types/params.go index 4d0768feac25..2667f002be21 100644 --- a/x/auth/types/params.go +++ b/x/auth/types/params.go @@ -63,11 +63,11 @@ func ParamKeyTable() subspace.KeyTable { // nolint func (p *Params) ParamSetPairs() subspace.ParamSetPairs { return subspace.ParamSetPairs{ - {KeyMaxMemoCharacters, &p.MaxMemoCharacters}, - {KeyTxSigLimit, &p.TxSigLimit}, - {KeyTxSizeCostPerByte, &p.TxSizeCostPerByte}, - {KeySigVerifyCostED25519, &p.SigVerifyCostED25519}, - {KeySigVerifyCostSecp256k1, &p.SigVerifyCostSecp256k1}, + {Key: KeyMaxMemoCharacters, Value: &p.MaxMemoCharacters}, + {Key: KeyTxSigLimit, Value: &p.TxSigLimit}, + {Key: KeyTxSizeCostPerByte, Value: &p.TxSizeCostPerByte}, + {Key: KeySigVerifyCostED25519, Value: &p.SigVerifyCostED25519}, + {Key: KeySigVerifyCostSecp256k1, Value: &p.SigVerifyCostSecp256k1}, } } diff --git a/x/auth/types/stdsignmsg.go b/x/auth/types/stdsignmsg.go index e018dac40048..8f9af05dc7f0 100644 --- a/x/auth/types/stdsignmsg.go +++ b/x/auth/types/stdsignmsg.go @@ -16,7 +16,7 @@ type StdSignMsg struct { Memo string `json:"memo" yaml:"memo"` } -// get message bytes +// Bytes get message bytes func (msg StdSignMsg) Bytes() []byte { return StdSignBytes(msg.ChainID, msg.AccountNumber, msg.Sequence, msg.Fee, msg.Msgs, msg.Memo) } diff --git a/x/auth/types/stdtx.go b/x/auth/types/stdtx.go index 9e71b1127d46..58f105ff053c 100644 --- a/x/auth/types/stdtx.go +++ b/x/auth/types/stdtx.go @@ -27,6 +27,7 @@ type StdTx struct { Memo string `json:"memo" yaml:"memo"` } +// NewStdTx returns a new StdTx func NewStdTx(msgs []sdk.Msg, fee StdFee, sigs []StdSignature, memo string) StdTx { return StdTx{ Msgs: msgs, diff --git a/x/genaccounts/internal/types/genesis_account.go b/x/genaccounts/internal/types/genesis_account.go index 1093c7d55462..0ca635e2f599 100644 --- a/x/genaccounts/internal/types/genesis_account.go +++ b/x/genaccounts/internal/types/genesis_account.go @@ -20,12 +20,12 @@ type GenesisAccount struct { AccountNumber uint64 `json:"account_number" yaml:"account_number"` // vesting account fields - OriginalVesting sdk.Coins `json:"original_vesting" yaml:"original_vesting"` // total vesting coins upon initialization - DelegatedFree sdk.Coins `json:"delegated_free" yaml:"delegated_free"` // delegated vested coins at time of delegation - DelegatedVesting sdk.Coins `json:"delegated_vesting" yaml:"delegated_vesting"` // delegated vesting coins at time of delegation - StartTime int64 `json:"start_time" yaml:"start_time"` // vesting start time (UNIX Epoch time) - EndTime int64 `json:"end_time" yaml:"end_time"` // vesting end time (UNIX Epoch time) - + OriginalVesting sdk.Coins `json:"original_vesting" yaml:"original_vesting"` // total vesting coins upon initialization + DelegatedFree sdk.Coins `json:"delegated_free" yaml:"delegated_free"` // delegated vested coins at time of delegation + DelegatedVesting sdk.Coins `json:"delegated_vesting" yaml:"delegated_vesting"` // delegated vesting coins at time of delegation + StartTime int64 `json:"start_time" yaml:"start_time"` // vesting start time (UNIX Epoch time) + EndTime int64 `json:"end_time" yaml:"end_time"` // vesting end time (UNIX Epoch time) + VestingPeriods authexported.VestingPeriods `json:"vesting_periods" yaml:"vesting_periods"` // custom vesting schedule // module account fields ModuleName string `json:"module_name" yaml:"module_name"` // name of the module account ModulePermissions []string `json:"module_permissions" yaml:"module_permissions"` // permissions of module account @@ -40,6 +40,15 @@ func (ga GenesisAccount) Validate() error { if ga.StartTime >= ga.EndTime { return errors.New("vesting start-time cannot be before end-time") } + if len(ga.VestingPeriods) > 0 { + endTime := ga.StartTime + for _, p := range ga.VestingPeriods { + endTime += p.PeriodLength + } + if endTime != ga.EndTime { + return errors.New("vesting end time does not match length of all vesting periods") + } + } } // don't allow blank (i.e just whitespaces) on the module name @@ -53,6 +62,7 @@ func (ga GenesisAccount) Validate() error { // NewGenesisAccountRaw creates a new GenesisAccount object func NewGenesisAccountRaw(address sdk.AccAddress, coins, vestingAmount sdk.Coins, vestingStartTime, vestingEndTime int64, + vestingPeriods authexported.VestingPeriods, module string, permissions ...string) GenesisAccount { return GenesisAccount{ @@ -65,6 +75,7 @@ func NewGenesisAccountRaw(address sdk.AccAddress, coins, DelegatedVesting: sdk.Coins{}, // ignored StartTime: vestingStartTime, EndTime: vestingEndTime, + VestingPeriods: vestingPeriods, ModuleName: module, ModulePermissions: permissions, } @@ -100,6 +111,7 @@ func NewGenesisAccountI(acc authexported.Account) (GenesisAccount, error) { gacc.DelegatedVesting = acc.GetDelegatedVesting() gacc.StartTime = acc.GetStartTime() gacc.EndTime = acc.GetEndTime() + gacc.VestingPeriods = acc.GetVestingPeriods() case supplyexported.ModuleAccountI: gacc.ModuleName = acc.GetName() gacc.ModulePermissions = acc.GetPermissions() @@ -120,6 +132,8 @@ func (ga *GenesisAccount) ToAccount() authexported.Account { ) switch { + case len(ga.VestingPeriods) > 0: + return auth.NewPeriodicVestingAccountRaw(baseVestingAcc, ga.StartTime, ga.VestingPeriods) case ga.StartTime != 0 && ga.EndTime != 0: return authtypes.NewContinuousVestingAccountRaw(baseVestingAcc, ga.StartTime) case ga.EndTime != 0: @@ -137,10 +151,10 @@ func (ga *GenesisAccount) ToAccount() authexported.Account { return bacc } -// GenesisAccounts defines a set of genesis account +// GenesisAccounts type alias for array of gensis accounts__________________________________ type GenesisAccounts []GenesisAccount -// Contains checks if a set of genesis accounts contain an address +// Contains check if genesis accounts contain an address func (gaccs GenesisAccounts) Contains(acc sdk.AccAddress) bool { for _, gacc := range gaccs { if gacc.Address.Equals(acc) { diff --git a/x/genaccounts/internal/types/genesis_account_test.go b/x/genaccounts/internal/types/genesis_account_test.go index 56dd140ec33a..def68303091c 100644 --- a/x/genaccounts/internal/types/genesis_account_test.go +++ b/x/genaccounts/internal/types/genesis_account_test.go @@ -8,9 +8,11 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" + tmtime "github.com/tendermint/tendermint/types/time" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" "github.com/cosmos/cosmos-sdk/x/supply" ) @@ -23,18 +25,18 @@ func TestGenesisAccountValidate(t *testing.T) { }{ { "valid account", - NewGenesisAccountRaw(addr, sdk.NewCoins(), sdk.NewCoins(), 0, 0, "", ""), + NewGenesisAccountRaw(addr, sdk.NewCoins(), sdk.NewCoins(), 0, 0, authexported.VestingPeriods{}, "", ""), nil, }, { "valid module account", - NewGenesisAccountRaw(addr, sdk.NewCoins(), sdk.NewCoins(), 0, 0, "mint", supply.Minter), + NewGenesisAccountRaw(addr, sdk.NewCoins(), sdk.NewCoins(), 0, 0, authexported.VestingPeriods{}, "mint", supply.Minter), nil, }, { "invalid vesting amount", NewGenesisAccountRaw(addr, sdk.NewCoins(sdk.NewInt64Coin("stake", 50)), - sdk.NewCoins(sdk.NewInt64Coin("stake", 100)), 0, 0, "", ""), + sdk.NewCoins(sdk.NewInt64Coin("stake", 100)), 0, 0, authexported.VestingPeriods{}, "", ""), errors.New("vesting amount cannot be greater than total amount"), }, { @@ -42,18 +44,24 @@ func TestGenesisAccountValidate(t *testing.T) { NewGenesisAccountRaw(addr, sdk.NewCoins(sdk.NewInt64Coin("uatom", 50), sdk.NewInt64Coin("eth", 50)), sdk.NewCoins(sdk.NewInt64Coin("uatom", 100), sdk.NewInt64Coin("eth", 20)), - 0, 0, "", ""), + 0, 0, authexported.VestingPeriods{}, "", ""), errors.New("vesting amount cannot be greater than total amount"), }, { "invalid vesting times", NewGenesisAccountRaw(addr, sdk.NewCoins(sdk.NewInt64Coin("stake", 50)), - sdk.NewCoins(sdk.NewInt64Coin("stake", 50)), 1654668078, 1554668078, "", ""), + sdk.NewCoins(sdk.NewInt64Coin("stake", 50)), 1654668078, 1554668078, authexported.VestingPeriods{}, "", ""), errors.New("vesting start-time cannot be before end-time"), }, + { + "vesting periods do not match end time", + NewGenesisAccountRaw(addr, sdk.NewCoins(sdk.NewInt64Coin("stake", 50)), + sdk.NewCoins(sdk.NewInt64Coin("stake", 50)), 1654668078, 1654669078, authexported.VestingPeriods{authexported.VestingPeriod{PeriodLength: int64(2000), VestingAmount: sdk.Coins{sdk.NewInt64Coin("stake", 50)}}}, "", ""), + errors.New("vesting end time does not match length of all vesting periods"), + }, { "invalid module account name", - NewGenesisAccountRaw(addr, sdk.NewCoins(), sdk.NewCoins(), 0, 0, " ", ""), + NewGenesisAccountRaw(addr, sdk.NewCoins(), sdk.NewCoins(), 0, 0, authexported.VestingPeriods{}, " ", ""), errors.New("module account name cannot be blank"), }, } @@ -87,6 +95,22 @@ func TestToAccount(t *testing.T) { require.IsType(t, &authtypes.ContinuousVestingAccount{}, acc) require.Equal(t, vacc, acc.(*authtypes.ContinuousVestingAccount)) + // periodic vesting account + now := tmtime.Now() + periods := authexported.VestingPeriods{ + authexported.VestingPeriod{int64(12 * 60 * 60), sdk.Coins{sdk.NewInt64Coin("fee", 500), sdk.NewInt64Coin("stake", 50)}}, + authexported.VestingPeriod{int64(6 * 60 * 60), sdk.Coins{sdk.NewInt64Coin("fee", 250), sdk.NewInt64Coin("stake", 25)}}, + authexported.VestingPeriod{int64(6 * 60 * 60), sdk.Coins{sdk.NewInt64Coin("fee", 250), sdk.NewInt64Coin("stake", 25)}}, + } + pacc := auth.NewPeriodicVestingAccount( + &authAcc, now.Unix(), periods, + ) + genAcc, err = NewGenesisAccountI(pacc) + require.NoError(t, err) + acc = genAcc.ToAccount() + require.IsType(t, &auth.PeriodicVestingAccount{}, acc) + require.Equal(t, pacc, acc.(*auth.PeriodicVestingAccount)) + // module account macc := supply.NewEmptyModuleAccount("mint", supply.Minter) genAcc, err = NewGenesisAccountI(macc) diff --git a/x/genaccounts/internal/types/genesis_state.go b/x/genaccounts/internal/types/genesis_state.go index c639be138c16..b2161b168f64 100644 --- a/x/genaccounts/internal/types/genesis_state.go +++ b/x/genaccounts/internal/types/genesis_state.go @@ -9,10 +9,10 @@ import ( "github.com/cosmos/cosmos-sdk/codec" ) -// GenesisState is a wrapper for GenAccounts +// GenesisState type alias for GenesisAccounts. The state to Unmarshal type GenesisState GenesisAccounts -// GetGenesisStateFromAppState gets the genesis state from the expected app state +// GetGenesisStateFromAppState get the genesis state from the expected app state func GetGenesisStateFromAppState(cdc *codec.Codec, appState map[string]json.RawMessage) GenesisState { var genesisState GenesisState if appState[ModuleName] != nil { @@ -22,7 +22,7 @@ func GetGenesisStateFromAppState(cdc *codec.Codec, appState map[string]json.RawM return genesisState } -// SetGenesisStateInAppState sets the genesis state within the expected app state +// SetGenesisStateInAppState set the genesis state within the expected app state func SetGenesisStateInAppState(cdc *codec.Codec, appState map[string]json.RawMessage, genesisState GenesisState) map[string]json.RawMessage {