Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Vesting Implementation #2168

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dd75176
start vesting implementation
AdityaSripal Aug 28, 2018
7caea52
Merge branch 'develop' of https://github.com/cosmos/cosmos-sdk into a…
AdityaSripal Aug 28, 2018
4543dcf
fix method signatures
AdityaSripal Aug 28, 2018
4c21c59
Switch float to Decimal. Keeper tests
AdityaSripal Sep 1, 2018
5703eae
delay transfer tests
AdityaSripal Sep 4, 2018
0215365
minor fixes and test docs
AdityaSripal Sep 4, 2018
a55164a
fmt
AdityaSripal Sep 4, 2018
bab8b34
Merge branch 'develop' into aditya/vesting
Sep 17, 2018
3a8235d
started changes to spec
AdityaSripal Sep 24, 2018
91547f4
Merge branch 'develop' into aditya/vesting
Sep 26, 2018
778b53d
Fix linting and minor cleanup of banking keeper
Sep 26, 2018
c1935a4
Cleanup vesting spec formatting
Sep 26, 2018
73224dd
Cleanup SendableCoins to make it easier to understand
Sep 26, 2018
d119e0f
fix error log
Sep 26, 2018
63c6f4a
Fix TestSubtractVestingFull
Sep 26, 2018
c1d746f
Actually fix TestSubtractVestingFull and more cleanup
Sep 26, 2018
b3bb5b1
Existing unit test cleanup
Sep 26, 2018
748cd14
More vesting unit test cleanup
Sep 27, 2018
aff8b0e
Add contracts for bank keeper methods
Sep 27, 2018
abd3eec
Update bank keeper interface to include DelegateCoins and DeductFees
Sep 27, 2018
c30da51
Implement unit test for bank keeper delegate coins
Sep 27, 2018
4b9d008
Implement unit test for bank keeper deduct fees
Sep 27, 2018
98d475c
Cleanup testing codec for auth structures
Sep 28, 2018
2412404
Implement LockedCoins
Sep 28, 2018
18aad2d
Merge branch 'develop' into aditya/vesting
Sep 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 70 additions & 47 deletions docs/spec/auth/vesting.md
Original file line number Diff line number Diff line change
@@ -1,106 +1,128 @@
## Vesting
# Vesting

### Intro and Requirements
## Intro and Requirements

This paper specifies vesting account implementation for the Cosmos Hub.
The requirements for this vesting account is that it should be initialized during genesis with
a starting balance X coins and a vesting endtime T. The owner of this account should be able to delegate to validators
and vote with locked coins, however they cannot send locked coins to other accounts until those coins have been unlocked.
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
This paper specifies vesting account implementation for the Cosmos Hub.
The requirements for a vesting account is that it should be initialized during
genesis with a starting balance `X` coins and a vesting end time `T`.

The owner of this account should be able to delegate to validators and vote with
locked coins, however they cannot send locked coins to other accounts until
those coins have been unlocked. 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.

### Implementation
## Implementation

##### Vesting Account implementation
### Vesting Account implementation

NOTE: `Now = ctx.BlockHeader().Time`
Given, `Now = ctx.BlockHeader().Time`

```go
type VestingAccount interface {
Account
AssertIsVestingAccount() // existence implies that account is vesting.
AssertIsVestingAccount() // existence implies that account is vesting

// Calculates amount of coins that can be sent to other accounts given the current time
// Calculates amount of coins that can be sent to other accounts
// given the current time.
SendableCoins(sdk.Context) sdk.Coins
}

// Implements Vesting Account
// Continuously vests by unlocking coins linearly with respect to time
type ContinuousVestingAccount struct {
BaseAccount
OriginalVestingCoins sdk.Coins // Coins in account on Initialization
ReceivedCoins sdk.Coins // Coins received from other accounts
SentCoins sdk.Coins // Coins sent to other accounts
OriginalVestingCoins sdk.Coins // coins in account on initialization
ReceivedCoins sdk.Coins // coins received from other accounts
SentCoins sdk.Coins // coins sent to other accounts

// StartTime and EndTime used to calculate how much of OriginalCoins is unlocked at any given point
// StartTime and EndTime used to calculate how much of OriginalCoins is
// unlocked at any given point.
StartTime time.Time
EndTime time.Time
}

// Uses time in context to calculate total unlocked coins
// Uses time in context to calculate total unlocked coins.
SendableCoins(vacc ContinuousVestingAccount, ctx sdk.Context) sdk.Coins:

// Coins unlocked by vesting schedule
// coins unlocked by vesting schedule
unlockedCoins := ReceivedCoins - SentCoins + OriginalVestingCoins * (Now - StartTime) / (EndTime - StartTime)

// Must still check for currentCoins constraint since some unlocked coins may have been delegated.
// Must still check for currentCoins constraint since some unlocked coins
// may have been delegated.
currentCoins := vacc.BaseAccount.GetCoins()

// min will return sdk.Coins with each denom having the minimum amount from unlockedCoins and currentCoins
// min will return sdk.Coins with each denom having the minimum amount from
// unlockedCoins and currentCoins
return min(unlockedCoins, currentCoins)

```

The `VestingAccount` interface is used to assert that an account is a vesting account like so:
The `VestingAccount` interface is used to assert that an account is a vesting
account like so:

```go
vacc, ok := acc.(VestingAccount); ok
```

as well as to calculate the SendableCoins at any given moment.

The `ContinuousVestingAccount` struct implements the Vesting account interface. It uses `OriginalVestingCoins`, `ReceivedCoins`,
`SentCoins`, `StartTime`, and `EndTime` to calculate how many coins are sendable at any given point.
Since the vesting restrictions need to be implemented on a per-module basis, the `ContinuousVestingAccount` implements
the `Account` interface exactly like `BaseAccount`. Thus, `ContinuousVestingAccount.GetCoins()` will return the total of
both locked coins and unlocked coins currently in the account. Delegated coins are deducted from `Account.GetCoins()`, but do not count against unlocked coins because they are still at stake and will be reinstated (partially if slashed) after waiting the full unbonding period.
The `ContinuousVestingAccount` struct implements the Vesting account interface.
It uses `OriginalVestingCoins`, `ReceivedCoins`, `SentCoins`, `StartTime`, and
`EndTime` to calculate how many coins are sendable at any given point.

Since the vesting restrictions need to be implemented on a per-module basis, the `ContinuousVestingAccount` implements the `Account` interface exactly like
`BaseAccount`. Thus, `ContinuousVestingAccount.GetCoins()` will return the total
of both locked coins and unlocked coins currently in the account. Delegated
coins are deducted from `Account.GetCoins()`, but do not count against unlocked
coins because they are still at stake and will be reinstated (partially if slashed)
after waiting the full unbonding period.

##### Changes to Keepers/Handler
### Changes to Keepers/Handler

Since a vesting account should be capable of doing everything but sending with its locked coins, the restriction should be
handled at the `bank.Keeper` level. Specifically in methods that are explicitly used for sending like
`sendCoins` and `inputOutputCoins`. These methods must check that an account is a vesting account using the check described above.
Since a vesting account should be capable of doing everything but sending with
its locked coins, the restriction should be handled at the `bank.Keeper` level.
Specifically in methods that are explicitly used for sending like `sendCoins` and
`inputOutputCoins`. These methods must check that an account is a vesting account
using the check described above.

```go
if acc is VestingAccount and Now < vestingAccount.EndTime:
// Check if amount is less than currently allowed sendable coins
// check if amount is less than currently allowed sendable coins
if msg.Amount > vestingAccount.SendableCoins(ctx) then fail
else:
vestingAccount.SentCoins += msg.Amount

else:
// Account has fully vested, treat like regular account
// account has fully vested, treat like regular account
if msg.Amount > account.GetCoins() then fail

// All checks passed, send the coins
// all checks passed, send the coins
SendCoins(inputs, outputs)

```

Coins that are sent to a vesting account after initialization by users sending them coins should be spendable
immediately after receiving them. Thus, handlers (like staking or bank) that send coins that a vesting account did not
originally own should increment `ReceivedCoins` by the amount sent.
Unlocked coins that are sent to other accounts will increment the vesting account's `SentCoins` attribute.
Coins that are sent to a vesting account after initialization by users sending
them coins should be spendable immediately after receiving them. Thus, handlers
(like staking or bank) that send coins that a vesting account did not originally
own should increment `ReceivedCoins` by the amount sent.

Unlocked coins that are sent to other accounts will increment the vesting
account's `SentCoins` attribute.

CONTRACT: Handlers SHOULD NOT update `ReceivedCoins` if they were originally sent from the vesting account. For example, if a vesting account unbonds from a validator, their tokens should be added back to account but staking handlers SHOULD NOT update `ReceivedCoins`.
However when a user sends coins to vesting account, then `ReceivedCoins` SHOULD be incremented.
CONTRACT: Handlers SHOULD NOT update `ReceivedCoins` if they were originally
sent from the vesting account. For example, if a vesting account unbonds from a
validator, their tokens should be added back to account but staking handlers
SHOULD NOT update `ReceivedCoins`.

However, when a user sends coins to vesting account, then `ReceivedCoins` SHOULD
be incremented.

### Initializing at Genesis

To initialize both vesting accounts and base accounts, the `GenesisAccount` struct will include an EndTime. Accounts meant to be
BaseAccounts will have `EndTime = 0`. The `initChainer` method will parse the GenesisAccount into BaseAccounts and VestingAccounts
as appropriate.
To initialize both vesting accounts and base accounts, the `GenesisAccount` struct
will include an `EndTime`. Accounts meant to be BaseAccounts will have `EndTime = 0`.

The `initChainer` method will parse the GenesisAccount into BaseAccounts and
VestingAccounts as appropriate.

```go
type GenesisAccount struct {
Expand All @@ -115,6 +137,7 @@ initChainer:
Address: gacc.Address,
Coins: gacc.GenesisCoins,
}

if gacc.EndTime != 0:
vestingAccount := ContinuouslyVestingAccount{
BaseAccount: baseAccount,
Expand All @@ -123,9 +146,9 @@ initChainer:
EndTime: gacc.EndTime,
}
AddAccountToState(vestingAccount)

else:
AddAccountToState(baseAccount)

```

### Formulas
Expand Down
12 changes: 10 additions & 2 deletions examples/democoin/x/cool/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import (
bank "github.com/cosmos/cosmos-sdk/x/bank"
)

func newTestCodec() *codec.Codec {
cdc := codec.New()

auth.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)

return cdc
}

func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) {
db := dbm.NewMemDB()
capKey := sdk.NewKVStoreKey("capkey")
Expand All @@ -26,8 +35,7 @@ func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) {

func TestCoolKeeper(t *testing.T) {
ms, capKey := setupMultiStore()
cdc := codec.New()
auth.RegisterBaseAccount(cdc)
cdc := newTestCodec()

am := auth.NewAccountMapper(cdc, capKey, auth.ProtoBaseAccount)
ctx := sdk.NewContext(ms, abci.Header{}, false, nil)
Expand Down
12 changes: 10 additions & 2 deletions examples/democoin/x/pow/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ import (
bank "github.com/cosmos/cosmos-sdk/x/bank"
)

func newTestCodec() *codec.Codec {
cdc := codec.New()

auth.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)

return cdc
}

func TestPowHandler(t *testing.T) {
ms, capKey := setupMultiStore()
cdc := codec.New()
auth.RegisterBaseAccount(cdc)
cdc := newTestCodec()

am := auth.NewAccountMapper(cdc, capKey, auth.ProtoBaseAccount)
ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger())
Expand Down
4 changes: 1 addition & 3 deletions examples/democoin/x/pow/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
dbm "github.com/tendermint/tendermint/libs/db"
"github.com/tendermint/tendermint/libs/log"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
auth "github.com/cosmos/cosmos-sdk/x/auth"
Expand All @@ -29,8 +28,7 @@ func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) {

func TestPowKeeperGetSet(t *testing.T) {
ms, capKey := setupMultiStore()
cdc := codec.New()
auth.RegisterBaseAccount(cdc)
cdc := newTestCodec()

am := auth.NewAccountMapper(cdc, capKey, auth.ProtoBaseAccount)
ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger())
Expand Down
15 changes: 11 additions & 4 deletions examples/democoin/x/simplestake/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import (
"github.com/cosmos/cosmos-sdk/x/bank"
)

func newTestCodec() *codec.Codec {
cdc := codec.New()

auth.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)

return cdc
}

func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) {
db := dbm.NewMemDB()
authKey := sdk.NewKVStoreKey("authkey")
Expand All @@ -32,8 +41,7 @@ func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) {

func TestKeeperGetSet(t *testing.T) {
ms, authKey, capKey := setupMultiStore()
cdc := codec.New()
auth.RegisterBaseAccount(cdc)
cdc := newTestCodec()

accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount)
stakeKeeper := NewKeeper(capKey, bank.NewBaseKeeper(accountMapper), DefaultCodespace)
Expand All @@ -60,8 +68,7 @@ func TestKeeperGetSet(t *testing.T) {

func TestBonding(t *testing.T) {
ms, authKey, capKey := setupMultiStore()
cdc := codec.New()
auth.RegisterBaseAccount(cdc)
cdc := newTestCodec()

ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger())

Expand Down
9 changes: 6 additions & 3 deletions server/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package server

import (
"bytes"
"io"
"os"
"testing"

"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server/mock"

"github.com/stretchr/testify/require"

tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands"
"github.com/tendermint/tendermint/libs/log"
"io"
"os"
"testing"
)

func TestEmptyState(t *testing.T) {
Expand Down
Loading