diff --git a/docs/spec/auth/vesting.md b/docs/spec/auth/vesting.md index c5c25ecaed03..8ecfd5d18b96 100644 --- a/docs/spec/auth/vesting.md +++ b/docs/spec/auth/vesting.md @@ -1,27 +1,31 @@ -## 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 } @@ -29,30 +33,32 @@ type VestingAccount interface { // 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 @@ -60,47 +66,63 @@ 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 { @@ -115,6 +137,7 @@ initChainer: Address: gacc.Address, Coins: gacc.GenesisCoins, } + if gacc.EndTime != 0: vestingAccount := ContinuouslyVestingAccount{ BaseAccount: baseAccount, @@ -123,9 +146,9 @@ initChainer: EndTime: gacc.EndTime, } AddAccountToState(vestingAccount) + else: AddAccountToState(baseAccount) - ``` ### Formulas diff --git a/examples/democoin/x/cool/keeper_test.go b/examples/democoin/x/cool/keeper_test.go index e3af7790e4b2..c6e18f2f1787 100644 --- a/examples/democoin/x/cool/keeper_test.go +++ b/examples/democoin/x/cool/keeper_test.go @@ -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") @@ -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) diff --git a/examples/democoin/x/pow/handler_test.go b/examples/democoin/x/pow/handler_test.go index 8166ddfc5321..3afaf4791219 100644 --- a/examples/democoin/x/pow/handler_test.go +++ b/examples/democoin/x/pow/handler_test.go @@ -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()) diff --git a/examples/democoin/x/pow/keeper_test.go b/examples/democoin/x/pow/keeper_test.go index dbd974c4d076..a8413c12deb0 100644 --- a/examples/democoin/x/pow/keeper_test.go +++ b/examples/democoin/x/pow/keeper_test.go @@ -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" @@ -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()) diff --git a/examples/democoin/x/simplestake/keeper_test.go b/examples/democoin/x/simplestake/keeper_test.go index 68f28bd91b07..7c36d5cb1556 100644 --- a/examples/democoin/x/simplestake/keeper_test.go +++ b/examples/democoin/x/simplestake/keeper_test.go @@ -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") @@ -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) @@ -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()) diff --git a/server/export_test.go b/server/export_test.go index 999ba3c00581..2904b2803893 100644 --- a/server/export_test.go +++ b/server/export_test.go @@ -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) { diff --git a/x/auth/account.go b/x/auth/account.go index 4a55a48ea3df..12b263666dde 100644 --- a/x/auth/account.go +++ b/x/auth/account.go @@ -2,8 +2,8 @@ package auth import ( "errors" + "time" - "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/tendermint/crypto" ) @@ -34,11 +34,13 @@ type Account interface { // AccountDecoder unmarshals account bytes type AccountDecoder func(accountBytes []byte) (Account, error) +var _ Account = (*BaseAccount)(nil) +var _ VestingAccount = (*ContinuousVestingAccount)(nil) +var _ VestingAccount = (*DelayTransferAccount)(nil) + //----------------------------------------------------------- // BaseAccount -var _ Account = (*BaseAccount)(nil) - // BaseAccount - a base account structure. // This can be extended by embedding within in your AppAccount. // There are examples of this in: examples/basecoin/types/account.go. @@ -121,12 +123,169 @@ func (acc *BaseAccount) SetSequence(seq int64) error { return nil } -//---------------------------------------- -// Wire +// VestingAccount is an account that can define a vesting schedule. Vesting coins +// can still be delegated, but only transferred after they have vested. +type VestingAccount interface { + Account + + // Returns true if account is still vesting, else false + // + // CONTRACT: After account is done vesting, account behaves exactly like + // BaseAccount. + IsVesting(time.Time) bool + + // Calculates amount of coins that can be sent to other accounts given the + // current blocktime. + SendableCoins(time.Time) sdk.Coins + + // Calculates the amount of coins that are locked in the vesting account. + LockedCoins(time.Time) sdk.Coins + + // Called on bank transfer functions (e.g. bank.SendCoins and bank.InputOutputCoins) + // Used to track coins that are transferred in and out of vesting account + // after initialization while account is still vesting. + TrackTransfers(sdk.Coins) +} + +// Implement Vesting Interface. Continuously vests coins linearly from +// StartTime until EndTime. +type ContinuousVestingAccount struct { + BaseAccount + OriginalVestingCoins sdk.Coins // coins in account on Initialization + TransferredCoins sdk.Coins // Net coins transferred into and out of account. May be negative. + + // StartTime and EndTime used to calculate how much of OriginalCoins is + // unlocked at any given point. + StartTime time.Time + EndTime time.Time +} + +func NewContinuousVestingAccount( + addr sdk.AccAddress, originalCoins sdk.Coins, + startTime, endTime time.Time) ContinuousVestingAccount { + + bacc := BaseAccount{ + Address: addr, + Coins: originalCoins, + } + return ContinuousVestingAccount{ + BaseAccount: bacc, + OriginalVestingCoins: originalCoins, + StartTime: startTime, + EndTime: endTime, + } +} + +// Implements VestingAccount interface. +func (cva ContinuousVestingAccount) IsVesting(blockTime time.Time) bool { + return blockTime.Unix() < cva.EndTime.Unix() +} + +// Implement Vesting Account interface. Uses time in context to calculate how +// many coins has been released by vesting schedule and then accounts for +// unlocked coins that have already been transferred or delegated. +func (cva ContinuousVestingAccount) SendableCoins(blockTime time.Time) sdk.Coins { + unlockedCoins := cva.TransferredCoins + + x := blockTime.Unix() - cva.StartTime.Unix() + y := cva.EndTime.Unix() - cva.StartTime.Unix() + scale := sdk.NewDec(x).Quo(sdk.NewDec(y)) + + // add original coins unlocked by vesting schedule + for _, origVestingCoin := range cva.OriginalVestingCoins { + vAmt := sdk.NewDecFromInt(origVestingCoin.Amount).Mul(scale).RoundInt() + + // Must constrain with coins left in account since some unlocked coins may + // have left account due to delegation. + currentAmount := cva.GetCoins().AmountOf(origVestingCoin.Denom) + + if currentAmount.LT(vAmt) { + vAmt = currentAmount + // prevent double count of transferred coins + vAmt = vAmt.Sub(cva.TransferredCoins.AmountOf(origVestingCoin.Denom)) + } + + // add non-zero coins + if !vAmt.IsZero() { + coin := sdk.NewCoin(origVestingCoin.Denom, vAmt) + unlockedCoins = unlockedCoins.Plus(sdk.Coins{coin}) + } + } + + return unlockedCoins +} + +// LockedCoins returns the amount of coins that are locked in the vesting account. +func (cva ContinuousVestingAccount) LockedCoins(blockTime time.Time) sdk.Coins { + return cva.GetCoins().Minus(cva.SendableCoins(blockTime)) +} + +// Implement Vesting Account. Track transfers in and out of account. +// +// CONTRACT: Send amounts must be negated. +func (cva *ContinuousVestingAccount) TrackTransfers(coins sdk.Coins) { + cva.TransferredCoins = cva.TransferredCoins.Plus(coins) +} + +// Implements Vesting Account. Vests all original coins after EndTime but keeps +// them all locked until that point. +type DelayTransferAccount struct { + BaseAccount + TransferredCoins sdk.Coins // Any received coins are sendable immediately + + // All coins unlocked after EndTime + EndTime time.Time +} + +func NewDelayTransferAccount(addr sdk.AccAddress, originalCoins sdk.Coins, endTime time.Time) DelayTransferAccount { + bacc := BaseAccount{ + Address: addr, + Coins: originalCoins, + } + return DelayTransferAccount{ + BaseAccount: bacc, + EndTime: endTime, + } +} + +// Implements VestingAccount. It returns if the account is still vesting. +func (dta DelayTransferAccount) IsVesting(blockTime time.Time) bool { + return blockTime.Unix() < dta.EndTime.Unix() +} + +// Implements VestingAccount. If Time < EndTime return only net transferred coins +// else return all coins in account (like BaseAccount). +func (dta DelayTransferAccount) SendableCoins(blockTime time.Time) sdk.Coins { + if blockTime.Unix() < dta.EndTime.Unix() { + sendableCoins := dta.TransferredCoins + + // Return net transferred coins if positive, then those coins are sendable. + for _, transCoin := range dta.TransferredCoins { + // Must constrain with coins left in account since some unlocked coins may + // have left account due to delegation. + amt := sendableCoins.AmountOf(transCoin.Denom) + + currentAmount := dta.GetCoins().AmountOf(transCoin.Denom) + if currentAmount.LT(amt) { + delta := sdk.NewCoin(transCoin.Denom, amt.Sub(currentAmount)) + sendableCoins = sendableCoins.Minus(sdk.Coins{delta}) + } + } + + return sendableCoins + } + + // if EndTime has passed, DelayTransferAccount behaves like BaseAccount + return dta.BaseAccount.GetCoins() +} + +// LockedCoins returns the amount of coins that are locked in the vesting account. +func (dta DelayTransferAccount) LockedCoins(blockTime time.Time) sdk.Coins { + return dta.GetCoins().Minus(dta.SendableCoins(blockTime)) +} -// Most users shouldn't use this, but this comes in handy for tests. -func RegisterBaseAccount(cdc *codec.Codec) { - cdc.RegisterInterface((*Account)(nil), nil) - cdc.RegisterConcrete(&BaseAccount{}, "cosmos-sdk/BaseAccount", nil) - codec.RegisterCrypto(cdc) +// Implement Vesting Account. Track transfers in and out of account send amounts +// must be negated. +func (dta *DelayTransferAccount) TrackTransfers(coins sdk.Coins) { + dta.TransferredCoins = dta.TransferredCoins.Plus(coins) } diff --git a/x/auth/account_test.go b/x/auth/account_test.go index b7a78e2d2220..e6bd0ca81f07 100644 --- a/x/auth/account_test.go +++ b/x/auth/account_test.go @@ -1,7 +1,9 @@ package auth import ( + "fmt" "testing" + "time" "github.com/stretchr/testify/require" @@ -106,3 +108,88 @@ func TestBaseAccountMarshal(t *testing.T) { err = cdc.UnmarshalBinary(b[:len(b)/2], &acc2) require.NotNil(t, err) } + +func TestSendableCoinsContinuousVesting(t *testing.T) { + cases := []struct { + blockTime time.Time + transferredCoins sdk.Coins + delegatedCoins sdk.Coins + expectedSendable sdk.Coins + }{ + // No tranfers + {time.Unix(0, 0), sdk.Coins(nil), sdk.Coins(nil), sdk.Coins(nil)}, // No coins available on initialization + {time.Unix(500, 0), sdk.Coins(nil), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(500)}}}, // Half coins available at halfway point + {time.Unix(1000, 0), sdk.Coins(nil), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(1000)}}}, // All coins available after EndTime + {time.Unix(2000, 0), sdk.Coins(nil), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(1000)}}}, // SendableCoins doesn't linearly increase after EndTime + + // Transfers + {time.Unix(0, 0), sdk.Coins{{"steak", sdk.NewInt(100)}}, sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(100)}}}, // Only transferred coins are sendable at time 0. + {time.Unix(500, 0), sdk.Coins{{"photon", sdk.NewInt(1000)}, {"steak", sdk.NewInt(100)}}, sdk.Coins(nil), sdk.Coins{{"photon", sdk.NewInt(1000)}, {"steak", sdk.NewInt(600)}}}, // scheduled coins + transferred coins + {time.Unix(500, 0), sdk.Coins{{"photon", sdk.NewInt(1000)}, {"steak", sdk.NewInt(-100)}}, sdk.Coins(nil), sdk.Coins{{"photon", sdk.NewInt(1000)}, {"steak", sdk.NewInt(400)}}}, // scheduled coins + transferred coins + + // Delegations + {time.Unix(500, 0), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(400)}}, sdk.Coins{{"steak", sdk.NewInt(500)}}}, // All delegated tokens are vesting + {time.Unix(500, 0), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(800)}}, sdk.Coins{{"steak", sdk.NewInt(200)}}}, // Some delegated tokens were unlocked (300) + {time.Unix(1000, 0), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(1000)}}, sdk.Coins(nil)}, // All coins are delegated + + // Integration Tests: Transfers and Delegations + {time.Unix(0, 0), sdk.Coins{{"photon", sdk.NewInt(10)}, {"steak", sdk.NewInt(10)}}, sdk.Coins{{"steak", sdk.NewInt(5)}}, sdk.Coins{{"photon", sdk.NewInt(10)}, {"steak", sdk.NewInt(10)}}}, // Delegate some of transferred coins + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(10)}}, sdk.Coins{{"steak", sdk.NewInt(400)}}, sdk.Coins{{"steak", sdk.NewInt(510)}}}, // Delegate locked coins only + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(10)}}, sdk.Coins{{"steak", sdk.NewInt(800)}}, sdk.Coins{{"steak", sdk.NewInt(210)}}}, // Delegate all locked coins and some unlocked coins + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(10)}}, sdk.Coins{{"steak", sdk.NewInt(1005)}}, sdk.Coins{{"steak", sdk.NewInt(5)}}}, // Delegate all locked coins and most of unlocked coins (including some transferred coins) + + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(-10)}}, sdk.Coins{{"steak", sdk.NewInt(400)}}, sdk.Coins{{"steak", sdk.NewInt(490)}}}, // Transfer unlocked coins, delegate locked coins + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(-10)}}, sdk.Coins{{"steak", sdk.NewInt(800)}}, sdk.Coins{{"steak", sdk.NewInt(190)}}}, // Transfer unlocked coins, delegate locked and unlocked coins + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(-10)}}, sdk.Coins{{"steak", sdk.NewInt(990)}}, sdk.Coins(nil)}, // Transfer unlocked coins, delegate rest of account + } + + for i, c := range cases { + _, _, addr := keyPubAddr() + vacc := NewContinuousVestingAccount(addr, sdk.Coins{{"steak", sdk.NewInt(1000)}}, time.Unix(0, 0), time.Unix(1000, 0)) + coins := vacc.GetCoins().Plus(c.transferredCoins) + coins = coins.Minus(c.delegatedCoins) // delegation is not tracked + vacc.SetCoins(coins) + vacc.TrackTransfers(c.transferredCoins) + + sendable := vacc.SendableCoins(c.blockTime) + require.Equal(t, c.expectedSendable, sendable, fmt.Sprintf("Expected sendablecoins is incorrect for testcase %d: {Transferred: %s, Delegated: %s, Time: %d", + i, c.transferredCoins, c.delegatedCoins, c.blockTime.Unix())) + } +} + +func TestSendableCoinsDelayTransfer(t *testing.T) { + cases := []struct { + blockTime time.Time + transferredCoins sdk.Coins + delegatedCoins sdk.Coins + expectedSendable sdk.Coins + }{ + // No transfers or delegations + {time.Unix(500, 0), sdk.Coins(nil), sdk.Coins(nil), sdk.Coins(nil)}, // Before EndTime. All coins locked + {time.Unix(1000, 0), sdk.Coins(nil), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(1000)}}}, // At Endtime, all coins unlocked + + // Transfers + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(100)}}, sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(100)}}}, // Transfer before EndTime + {time.Unix(1000, 0), sdk.Coins{{"steak", sdk.NewInt(100)}}, sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(1100)}}}, // Transfer after EndTime + + // Delegations + {time.Unix(1000, 0), sdk.Coins(nil), sdk.Coins{{"steak", sdk.NewInt(800)}}, sdk.Coins{{"steak", sdk.NewInt(200)}}}, // Some unlocked coins are delegated + + // Transfers and Delegations + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(500)}}, sdk.Coins{{"steak", sdk.NewInt(1000)}}, sdk.Coins{{"steak", sdk.NewInt(500)}}}, // Delegate all locked coins + {time.Unix(500, 0), sdk.Coins{{"steak", sdk.NewInt(500)}}, sdk.Coins{{"steak", sdk.NewInt(1300)}}, sdk.Coins{{"steak", sdk.NewInt(200)}}}, // Delegate all locked coins and some transferred coins + } + + for i, c := range cases { + _, _, addr := keyPubAddr() + vacc := NewDelayTransferAccount(addr, sdk.Coins{{"steak", sdk.NewInt(1000)}}, time.Unix(1000, 0)) + coins := vacc.GetCoins().Plus(c.transferredCoins) + coins = coins.Minus(c.delegatedCoins) // delegation is not tracked + vacc.SetCoins(coins) + vacc.TrackTransfers(c.transferredCoins) + + sendable := vacc.SendableCoins(c.blockTime) + require.Equal(t, c.expectedSendable, sendable, fmt.Sprintf("Expected sendablecoins is incorrect for testcase %d: {Transferred: %s, Delegated: %s, Time: %d", + i, c.transferredCoins, c.delegatedCoins, c.blockTime.Unix())) + } +} diff --git a/x/auth/ante_test.go b/x/auth/ante_test.go index 2a289f317bf8..b19a737f1e6f 100644 --- a/x/auth/ante_test.go +++ b/x/auth/ante_test.go @@ -4,7 +4,6 @@ import ( "fmt" "testing" - codec "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" abci "github.com/tendermint/tendermint/abci/types" @@ -108,10 +107,9 @@ func newTestTxWithSignBytes(msgs []sdk.Msg, privs []crypto.PrivKey, accNums []in // Test various error cases in the AnteHandler control flow. func TestAnteHandlerSigErrors(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -161,10 +159,9 @@ func TestAnteHandlerSigErrors(t *testing.T) { // Test logic around account number checking with one signer and many signers. func TestAnteHandlerAccountNumbers(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -220,10 +217,9 @@ func TestAnteHandlerAccountNumbers(t *testing.T) { // Test logic around sequence checking with one signer and many signers. func TestAnteHandlerSequences(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -298,10 +294,9 @@ func TestAnteHandlerSequences(t *testing.T) { // Test logic around fee deduction. func TestAnteHandlerFees(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -340,10 +335,9 @@ func TestAnteHandlerFees(t *testing.T) { // Test logic around memo gas consumption. func TestAnteHandlerMemoGas(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -383,10 +377,9 @@ func TestAnteHandlerMemoGas(t *testing.T) { } func TestAnteHandlerMultiSigner(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -434,10 +427,9 @@ func TestAnteHandlerMultiSigner(t *testing.T) { } func TestAnteHandlerBadSignBytes(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -515,10 +507,9 @@ func TestAnteHandlerBadSignBytes(t *testing.T) { } func TestAnteHandlerSetPubKey(t *testing.T) { - // setup ms, capKey, capKey2 := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) feeCollector := NewFeeCollectionKeeper(cdc, capKey2) anteHandler := NewAnteHandler(mapper, feeCollector) @@ -570,8 +561,8 @@ func TestAnteHandlerSetPubKey(t *testing.T) { func TestProcessPubKey(t *testing.T) { ms, capKey, _ := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() + mapper := NewAccountMapper(cdc, capKey, ProtoBaseAccount) ctx := sdk.NewContext(ms, abci.Header{ChainID: "mychainid"}, false, log.NewNopLogger()) // keys diff --git a/x/auth/codec.go b/x/auth/codec.go index 624bdf4288db..a3dffcdacc3b 100644 --- a/x/auth/codec.go +++ b/x/auth/codec.go @@ -4,15 +4,18 @@ import ( "github.com/cosmos/cosmos-sdk/codec" ) +var msgCdc = codec.New() + // Register concrete types on codec codec for default AppAccount func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*Account)(nil), nil) cdc.RegisterConcrete(&BaseAccount{}, "auth/Account", nil) cdc.RegisterConcrete(StdTx{}, "auth/StdTx", nil) + cdc.RegisterInterface((*VestingAccount)(nil), nil) + cdc.RegisterConcrete(&ContinuousVestingAccount{}, "auth/ContinuousVestingAccount", nil) + cdc.RegisterConcrete(&DelayTransferAccount{}, "auth/DelayTransferAccount", nil) } -var msgCdc = codec.New() - func init() { RegisterCodec(msgCdc) codec.RegisterCrypto(msgCdc) diff --git a/x/auth/mapper_test.go b/x/auth/mapper_test.go index 9580d3133d5c..1e28648aef93 100644 --- a/x/auth/mapper_test.go +++ b/x/auth/mapper_test.go @@ -14,6 +14,15 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +func newTestCodec() *codec.Codec { + cdc := codec.New() + + RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + + return cdc +} + func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { db := dbm.NewMemDB() capKey := sdk.NewKVStoreKey("capkey") @@ -27,8 +36,7 @@ func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey, *sdk.KVStoreKey) { func TestAccountMapperGetSet(t *testing.T) { ms, capKey, _ := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() // make context and mapper ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) @@ -63,8 +71,7 @@ func TestAccountMapperGetSet(t *testing.T) { func BenchmarkAccountMapperGetAccountFound(b *testing.B) { ms, capKey, _ := setupMultiStore() - cdc := codec.New() - RegisterBaseAccount(cdc) + cdc := newTestCodec() // make context and mapper ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) diff --git a/x/bank/keeper.go b/x/bank/keeper.go index 2da4eedc8b3f..8214da9b9fc8 100644 --- a/x/bank/keeper.go +++ b/x/bank/keeper.go @@ -19,9 +19,13 @@ const ( // between accounts. type Keeper interface { SendKeeper + SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error SubtractCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) + + DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) + DeductFees(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) } var _ Keeper = (*BaseKeeper)(nil) @@ -53,6 +57,10 @@ func (keeper BaseKeeper) HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk. } // SubtractCoins subtracts amt from the coins at the addr. +// +// CONTRACT: Under the context of a vesting account, SubtractCoins will also +// check if the account has enough unlocked coins to spend and will additionally +// track the transferred coins. func (keeper BaseKeeper) SubtractCoins( ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, ) (sdk.Coins, sdk.Tags, sdk.Error) { @@ -61,6 +69,9 @@ func (keeper BaseKeeper) SubtractCoins( } // AddCoins adds amt to the coins at the addr. +// +// CONTRACT: Under the context of a vesting account, AddCoins will also +// additionally track transferred coins. func (keeper BaseKeeper) AddCoins( ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, ) (sdk.Coins, sdk.Tags, sdk.Error) { @@ -68,7 +79,11 @@ func (keeper BaseKeeper) AddCoins( return addCoins(ctx, keeper.am, addr, amt) } -// SendCoins moves coins from one account to another +// SendCoins moves coins from one account to another. +// +// CONTRACT: Under the context of a vesting account for the from address, the +// contract of SubtractCoins applies and under the context of a vesting account +// for the to address, the contract of AddCoins applies. func (keeper BaseKeeper) SendCoins( ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins, ) (sdk.Tags, sdk.Error) { @@ -76,12 +91,34 @@ func (keeper BaseKeeper) SendCoins( return sendCoins(ctx, keeper.am, fromAddr, toAddr, amt) } -// InputOutputCoins handles a list of inputs and outputs +// InputOutputCoins handles a list of inputs and outputs. +// +// CONTRACT: Under the context of a vesting account for any address in the inputs, +// the contract of SubtractCoins applies and under the context of a vesting account +// for any address in the outputs, the contract of AddCoins applies. func (keeper BaseKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) { return inputOutputCoins(ctx, keeper.am, inputs, outputs) } -//______________________________________________________________________________________________ +// DelegateCoins implements the bank Keeper interface. It will remove coins from +// an account. +// +// CONTRACT: Under the context of a vesting account, it will remove coins +// without updating the tranferred amount. +func (keeper BaseKeeper) DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { + return delegateCoins(ctx, keeper.am, addr, amt) +} + +// DeductFees implements the bank Keeper interface. It will deduct fees from a +// given account. +// +// CONTRACT: Under the context of a vesting account, it it will remove vested +// coins before subtracting vesting coins. +func (keeper BaseKeeper) DeductFees(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { + return deductFees(ctx, keeper.am, addr, amt) +} + +//_____________________________________________________________________________ // SendKeeper defines a module interface that facilitates the transfer of coins // between accounts without the possibility of creating coins. @@ -130,7 +167,7 @@ func (keeper BaseSendKeeper) InputOutputCoins( return inputOutputCoins(ctx, keeper.am, inputs, outputs) } -//______________________________________________________________________________________________ +//_____________________________________________________________________________ // ViewKeeper defines a module interface that facilitates read only access to // account balances. @@ -161,28 +198,33 @@ func (keeper BaseViewKeeper) HasCoins(ctx sdk.Context, addr sdk.AccAddress, amt return hasCoins(ctx, keeper.am, addr, amt) } -//______________________________________________________________________________________________ +// Auxiliary functions +//_____________________________________________________________________________ func getCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress) sdk.Coins { ctx.GasMeter().ConsumeGas(costGetCoins, "getCoins") + acc := am.GetAccount(ctx, addr) if acc == nil { return sdk.Coins{} } + return acc.GetCoins() } func setCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, amt sdk.Coins) sdk.Error { ctx.GasMeter().ConsumeGas(costSetCoins, "setCoins") + acc := am.GetAccount(ctx, addr) if acc == nil { acc = am.NewAccountWithAddress(ctx, addr) } - err := acc.SetCoins(amt) - if err != nil { + + if err := acc.SetCoins(amt); err != nil { // Handle w/ #870 panic(err) } + am.SetAccount(ctx, acc) return nil } @@ -196,31 +238,66 @@ func hasCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, amt s // SubtractCoins subtracts amt from the coins at the addr. func subtractCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) { ctx.GasMeter().ConsumeGas(costSubtractCoins, "subtractCoins") + oldCoins := getCoins(ctx, am, addr) newCoins := oldCoins.Minus(amt) + if !newCoins.IsNotNegative() { return amt, nil, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", oldCoins, amt)) } + + blockTime := ctx.BlockHeader().Time + vacc, ok := am.GetAccount(ctx, addr).(auth.VestingAccount) + + // check if sender is vesting account + if ok && vacc.IsVesting(blockTime) { + // check if account has enough unlocked coins + sendableCoins := vacc.SendableCoins(blockTime) + if !sendableCoins.IsGTE(amt) { + return amt, nil, sdk.ErrInsufficientCoins("not enough sendable coins in vesting account") + } + + // track transfers + vacc.TrackTransfers(amt.Negative()) + am.SetAccount(ctx, vacc) + } + err := setCoins(ctx, am, addr, newCoins) tags := sdk.NewTags("sender", []byte(addr.String())) + return newCoins, tags, err } // AddCoins adds amt to the coins at the addr. func addCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) { ctx.GasMeter().ConsumeGas(costAddCoins, "addCoins") + oldCoins := getCoins(ctx, am, addr) newCoins := oldCoins.Plus(amt) + if !newCoins.IsNotNegative() { return amt, nil, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", oldCoins, amt)) } + + blockTime := ctx.BlockHeader().Time + vacc, ok := am.GetAccount(ctx, addr).(auth.VestingAccount) + + // update transferred coins for Vesting accounts + if ok && vacc.IsVesting(blockTime) { + // track transfers + vacc.TrackTransfers(amt) + am.SetAccount(ctx, vacc) + } + err := setCoins(ctx, am, addr, newCoins) tags := sdk.NewTags("recipient", []byte(addr.String())) + return newCoins, tags, err } -// SendCoins moves coins from one account to another -// NOTE: Make sure to revert state changes from tx on error +// SendCoins moves coins from one account to another. +// +// NOTE: Make sure to revert state changes from tx on error. func sendCoins(ctx sdk.Context, am auth.AccountMapper, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { _, subTags, err := subtractCoins(ctx, am, fromAddr, amt) if err != nil { @@ -235,8 +312,9 @@ func sendCoins(ctx sdk.Context, am auth.AccountMapper, fromAddr sdk.AccAddress, return subTags.AppendTags(addTags), nil } -// InputOutputCoins handles a list of inputs and outputs -// NOTE: Make sure to revert state changes from tx on error +// InputOutputCoins handles a list of inputs and outputs. +// +// NOTE: Make sure to revert state changes from tx on error. func inputOutputCoins(ctx sdk.Context, am auth.AccountMapper, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) { allTags := sdk.EmptyTags() @@ -245,6 +323,7 @@ func inputOutputCoins(ctx sdk.Context, am auth.AccountMapper, inputs []Input, ou if err != nil { return nil, err } + allTags = allTags.AppendTags(tags) } @@ -253,8 +332,70 @@ func inputOutputCoins(ctx sdk.Context, am auth.AccountMapper, inputs []Input, ou if err != nil { return nil, err } + allTags = allTags.AppendTags(tags) } return allTags, nil } + +// delegateCoins will remove coins from account without updating the tranferred +// amount. Thus, it will subtract vestING coins first before subtracting vestED +// coins. +func delegateCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { + ctx.GasMeter().ConsumeGas(costSubtractCoins, "subtractCoins") + + oldCoins := getCoins(ctx, am, addr) + newCoins := oldCoins.Minus(amt) + + if !newCoins.IsNotNegative() { + return nil, sdk.ErrInsufficientCoins("insufficient balance to delegate") + } + + err := setCoins(ctx, am, addr, newCoins) + tags := sdk.NewTags("sender", []byte(addr.String())) + + return tags, err +} + +// deductFees will deduct fees from a given account. If the account is a vesting +// account, it will remove vested coins before subtracting vesting coins. +func deductFees(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, fees sdk.Coins) (sdk.Tags, sdk.Error) { + ctx.GasMeter().ConsumeGas(costSubtractCoins, "subtractCoins") + + oldCoins := getCoins(ctx, am, addr) + newCoins := []sdk.Coin{} + + for _, fee := range fees { + blockTime := ctx.BlockHeader().Time + vacc, ok := am.GetAccount(ctx, addr).(auth.VestingAccount) + + if ok && vacc.IsVesting(blockTime) { + spendableCoins := vacc.SendableCoins(blockTime) + spendableAmount := spendableCoins.AmountOf(fee.Denom) + + // TODO: utilize GTE + if spendableAmount.GT(fee.Amount) || spendableAmount.Equal(fee.Amount) { + vacc.TrackTransfers([]sdk.Coin{fee}) + } else { + vacc.TrackTransfers([]sdk.Coin{sdk.NewCoin(fee.Denom, spendableAmount)}) + } + + am.SetAccount(ctx, vacc) + } + + accountAmount := oldCoins.AmountOf(fee.Denom) + if accountAmount.LT(fee.Amount) { + return nil, sdk.ErrInsufficientCoins("insufficient balance to pay fee") + } + + if accountAmount.GT(fee.Amount) { + newCoins = append(newCoins, sdk.NewCoin(fee.Denom, accountAmount.Sub(fee.Amount))) + } + } + + err := setCoins(ctx, am, addr, newCoins) + tags := sdk.NewTags("sender", []byte(addr.String())) + + return tags, err +} diff --git a/x/bank/keeper_test.go b/x/bank/keeper_test.go index c48410c154e3..6fc1022e3358 100644 --- a/x/bank/keeper_test.go +++ b/x/bank/keeper_test.go @@ -2,6 +2,7 @@ package bank import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -10,13 +11,26 @@ import ( dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" - codec "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" ) +var ( + testCoinDenom = "steak" +) + +func newTestCodec() *codec.Codec { + cdc := codec.New() + + auth.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + + return cdc +} + func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { db := dbm.NewMemDB() authKey := sdk.NewKVStoreKey("authkey") @@ -26,11 +40,17 @@ func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) { return ms, authKey } +func getVestingTotal(coin sdk.Coin, blockTime, vAccStartTime, vAccEndTime time.Time) sdk.Int { + x := blockTime.Unix() - vAccStartTime.Unix() + y := vAccEndTime.Unix() - vAccStartTime.Unix() + scale := sdk.NewDec(x).Quo(sdk.NewDec(y)) + + return sdk.NewDecFromInt(coin.Amount).Mul(scale).RoundInt() +} + func TestKeeper(t *testing.T) { ms, authKey := setupMultiStore() - - cdc := codec.New() - auth.RegisterBaseAccount(cdc) + cdc := newTestCodec() ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) @@ -113,9 +133,7 @@ func TestKeeper(t *testing.T) { func TestSendKeeper(t *testing.T) { ms, authKey := setupMultiStore() - - cdc := codec.New() - auth.RegisterBaseAccount(cdc) + cdc := newTestCodec() ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) @@ -182,9 +200,7 @@ func TestSendKeeper(t *testing.T) { func TestViewKeeper(t *testing.T) { ms, authKey := setupMultiStore() - - cdc := codec.New() - auth.RegisterBaseAccount(cdc) + cdc := newTestCodec() ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) @@ -207,3 +223,425 @@ func TestViewKeeper(t *testing.T) { require.False(t, viewKeeper.HasCoins(ctx, addr, sdk.Coins{sdk.NewInt64Coin("foocoin", 15)})) require.False(t, viewKeeper.HasCoins(ctx, addr, sdk.Coins{sdk.NewInt64Coin("barcoin", 5)})) } + +func TestVesting(t *testing.T) { + ms, authKey := setupMultiStore() + + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(500, 0)}, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + + vacc := auth.NewContinuousVestingAccount( + addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(100))}, time.Unix(0, 0), time.Unix(1000, 0), + ) + accountMapper.SetAccount(ctx, &vacc) + + // require that we cannot send more than sendable coins + _, err := bankKeeper.SendCoins(ctx, addr1, addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(70))}) + require.NotNil(t, err, "expected error on invalid transfer: %v", err) + require.Equal(t, sdk.CodeType(10), err.Code(), "failed to error with insufficient coins") + + // require that we can send less than sendable coins + _, err = bankKeeper.SendCoins(ctx, addr1, addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(40))}) + require.Nil(t, err, "expected error on valid transfer: %v", err) + + acc := accountMapper.GetAccount(ctx, addr1).(*auth.ContinuousVestingAccount) + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(-40))}, + acc.TransferredCoins, "failed to update transferred coins", + ) + + // receive coins + addr3 := sdk.AccAddress([]byte("addr3")) + acc3 := auth.NewBaseAccountWithAddress(addr3) + acc3.SetCoins(sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + accountMapper.SetAccount(ctx, &acc3) + + _, err = bankKeeper.SendCoins(ctx, addr3, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + acc = accountMapper.GetAccount(ctx, addr1).(*auth.ContinuousVestingAccount) + require.Nil(t, err, "unexpected error sending to a vesting account: %v", err) + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(10))}, + acc.TransferredCoins, "failed to transfer coins", + ) + + // send transferred coins + _, err = bankKeeper.SendCoins(ctx, addr1, addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(60))}) + require.Nil(t, err, "failed to send transferred coins: %v", err) + + acc = accountMapper.GetAccount(ctx, addr1).(*auth.ContinuousVestingAccount) + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(-50))}, + acc.TransferredCoins, "failed to update transferred coins", + ) +} + +func TestVestingInputOutput(t *testing.T) { + ms, authKey := setupMultiStore() + + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(500, 0)}, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + + vacc := auth.NewContinuousVestingAccount( + addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(100))}, time.Unix(0, 0), time.Unix(1000, 0), + ) + accountMapper.SetAccount(ctx, &vacc) + + // send some coins back to self to check if transferredCoins updates correctly + inputs := []Input{ + {addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}}, + } + outputs := []Output{ + {addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(20))}}, + {addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(30))}}, + } + + _, err := bankKeeper.InputOutputCoins(ctx, inputs, outputs) + require.Nil(t, err, "InputOutput failed on valid vested spend") + + acc := accountMapper.GetAccount(ctx, addr1).(*auth.ContinuousVestingAccount) + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(-30))}, + acc.TransferredCoins, "failed to update transferred coins", + ) +} + +func TestDelayTransferSend(t *testing.T) { + ms, authKey := setupMultiStore() + + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(500, 0)}, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + + dtacc := auth.NewDelayTransferAccount( + addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(100))}, time.Unix(1000, 0), + ) + accountMapper.SetAccount(ctx, &dtacc) + + acc := auth.NewBaseAccountWithAddress(addr2) + acc.SetCoins(sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + accountMapper.SetAccount(ctx, &acc) + + // send coins before EndTime fails + _, err := bankKeeper.SendCoins(ctx, addr1, addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(1))}) + require.NotNil(t, err, "expected keeper to fail sending locked coins") + require.Equal(t, sdk.CodeType(10), err.Code(), "failed error with insufficient coins") + + // receive coins + bankKeeper.SendCoins(ctx, addr2, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + + recoverAcc := accountMapper.GetAccount(ctx, addr1).(*auth.DelayTransferAccount) + require.Equal(t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}, + recoverAcc.TransferredCoins, "expected transferred coins to update correctly", + ) + + // spend some of received Coins + _, err = bankKeeper.SendCoins(ctx, addr1, addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(25))}) + + recoverAcc = accountMapper.GetAccount(ctx, addr1).(*auth.DelayTransferAccount) + require.Nil(t, err, "expected keeper to allow valid send") + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(25))}, + recoverAcc.TransferredCoins, "expected transferred coins to update correctly", + ) + + // fast-forward to EndTime + ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1000, 0)}) + + // spend all unlocked coins + _, err = bankKeeper.SendCoins(ctx, addr1, addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(125))}) + + recoverAcc = accountMapper.GetAccount(ctx, addr1).(*auth.DelayTransferAccount) + require.Nil(t, err, "expected keeper to allow valid send") + require.Equal(t, sdk.Coins(nil), recoverAcc.GetCoins(), "invalid amount of sendable coins") + require.False(t, recoverAcc.IsVesting(ctx.BlockHeader().Time), "account should not still be vesting after EndTime") +} + +func TestDelayTransferInputOutput(t *testing.T) { + ms, authKey := setupMultiStore() + + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + ctx := sdk.NewContext(ms, abci.Header{Time: time.Unix(500, 0)}, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + + vacc := auth.NewDelayTransferAccount( + addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(100))}, time.Unix(1000, 0), + ) + accountMapper.SetAccount(ctx, &vacc) + + acc := auth.NewBaseAccountWithAddress(addr2) + acc.SetCoins(sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + accountMapper.SetAccount(ctx, &acc) + + // transfer coins to delay transfer account + bankKeeper.SendCoins(ctx, addr2, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + + // send some coins back to self to check if transferredCoins updates correctly + inputs := []Input{ + {addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}}, + } + outputs := []Output{ + {addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(20))}}, + {addr2, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(30))}}, + } + + _, err := bankKeeper.InputOutputCoins(ctx, inputs, outputs) + require.Nil(t, err, "InputOutput failed on valid vested spend") + + recoverAcc := accountMapper.GetAccount(ctx, addr1).(*auth.DelayTransferAccount) + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(20))}, + recoverAcc.TransferredCoins, "expected transferred coins to update correctly", + ) +} + +func TestSubtractVestingFull(t *testing.T) { + ms, authKey := setupMultiStore() + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + header := abci.Header{Time: time.Now().UTC()} + ctx := sdk.NewContext(ms, header, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + + coin := sdk.NewCoin(testCoinDenom, sdk.NewInt(100)) + amt := sdk.Coins{coin} + + // create a vesting account that is fully vested + vAccStartTime := header.Time.Add(-72 * time.Hour) + vAccEndTime := header.Time + require.Equal(t, coin.Amount, getVestingTotal(coin, header.Time, vAccStartTime, vAccEndTime)) + + vacc := auth.NewContinuousVestingAccount(addr1, amt, vAccStartTime, vAccEndTime) + accountMapper.SetAccount(ctx, &vacc) + + // require that we be able to subtract the full desired amount + res, _, err := bankKeeper.SubtractCoins(ctx, addr1, amt) + require.Nil(t, err, "unexpected error: %v", err) + require.Equal(t, sdk.Coins(nil), res, "Coins did not update correctly") + + dtacc := auth.NewDelayTransferAccount(addr2, amt, header.Time) + accountMapper.SetAccount(ctx, &dtacc) + + // require that we be able to subtract the full desired amount as the end time + // has matured + res, _, err = bankKeeper.SubtractCoins(ctx, addr2, amt) + require.Nil(t, err, "unexpected error: %v", err) + require.Equal(t, sdk.Coins(nil), res, "Coins did not update correctly") +} + +func TestDelegateCoinsNonVesting(t *testing.T) { + ms, authKey := setupMultiStore() + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + header := abci.Header{Time: time.Now().UTC()} + ctx := sdk.NewContext(ms, header, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + + coin := sdk.NewCoin(testCoinDenom, sdk.NewInt(100)) + amt := sdk.Coins{coin} + + acc := accountMapper.NewAccountWithAddress(ctx, addr1) + accountMapper.SetAccount(ctx, acc) + bankKeeper.SetCoins(ctx, addr1, amt) + + // require non-vesting account cannot delegate beyond their balance + _, err := bankKeeper.DelegateCoins(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(101))}) + require.Error(t, err, "expected delegation to fail due to lack of funds") + + // require non-vesting account can delegate within their balance + _, err = bankKeeper.DelegateCoins(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + require.NoError(t, err, "unexpected delegation failure") + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}, + bankKeeper.GetCoins(ctx, addr1), "expected account balance to reflect delegation", + ) +} + +func TestDelegateCoinsVesting(t *testing.T) { + ms, authKey := setupMultiStore() + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + header := abci.Header{Time: time.Now().UTC()} + ctx := sdk.NewContext(ms, header, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + + coin := sdk.NewCoin(testCoinDenom, sdk.NewInt(100)) + amt := sdk.Coins{coin} + + vacc := auth.NewContinuousVestingAccount(addr1, amt, header.Time, header.Time) + accountMapper.SetAccount(ctx, &vacc) + origTransferred := vacc.TransferredCoins + + // require vesting account cannot delegate beyond their spendable balance + _, err := bankKeeper.DelegateCoins(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(101))}) + require.Error(t, err, "expected delegation to fail due to lack of funds") + + // require vesting account can delegate within their spendable balance + _, err = bankKeeper.DelegateCoins(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}) + require.NoError(t, err, "unexpected delegation failure") + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(50))}, + bankKeeper.GetCoins(ctx, addr1), "expected account balance to reflect delegation", + ) + + // require transferred coins have not changed + acc, ok := accountMapper.GetAccount(ctx, addr1).(*auth.ContinuousVestingAccount) + require.True(t, ok) + require.Equal(t, origTransferred, acc.TransferredCoins, "expected vesting account tranferred coins to remain unchanged") +} + +func TestDeductFeesNonVesting(t *testing.T) { + ms, authKey := setupMultiStore() + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + header := abci.Header{Time: time.Now().UTC()} + ctx := sdk.NewContext(ms, header, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + + coin := sdk.NewCoin(testCoinDenom, sdk.NewInt(5)) + amt := sdk.Coins{coin} + + acc := accountMapper.NewAccountWithAddress(ctx, addr1) + accountMapper.SetAccount(ctx, acc) + bankKeeper.SetCoins(ctx, addr1, amt) + + // require non-vesting account cannot pay for fees beyond their balance + _, err := bankKeeper.DeductFees(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(6))}) + require.Error(t, err, "expected fee payment to fail due to lack of funds") + + // require non-vesting account can pay for fees within their balance + _, err = bankKeeper.DeductFees(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(1))}) + require.NoError(t, err, "unexpected fee deduction failure") + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(4))}, + bankKeeper.GetCoins(ctx, addr1), "expected account balance to reflect fee deduction", + ) +} + +func TestDeductFeesVesting(t *testing.T) { + ms, authKey := setupMultiStore() + cdc := codec.New() + + codec.RegisterCrypto(cdc) + auth.RegisterCodec(cdc) + + header := abci.Header{Time: time.Now().UTC()} + ctx := sdk.NewContext(ms, header, false, log.NewNopLogger()) + accountMapper := auth.NewAccountMapper(cdc, authKey, auth.ProtoBaseAccount) + bankKeeper := NewBaseKeeper(accountMapper) + + addr1 := sdk.AccAddress([]byte("addr1")) + addr2 := sdk.AccAddress([]byte("addr2")) + addr3 := sdk.AccAddress([]byte("addr3")) + + coin := sdk.NewCoin(testCoinDenom, sdk.NewInt(5)) + amt := sdk.Coins{coin} + + vacc := auth.NewContinuousVestingAccount(addr1, amt, header.Time, header.Time) + accountMapper.SetAccount(ctx, &vacc) + + // require vesting account cannot pay for fees beyond their spendable balance + _, err := bankKeeper.DeductFees(ctx, addr1, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(6))}) + require.Error(t, err, "expected fee payment to fail due to lack of funds") + + vAccStartTime := header.Time.Add(-24 * time.Hour) + vAccEndTime := header.Time.Add(24 * time.Hour) + + fee := sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(1))} + vacc = auth.NewContinuousVestingAccount(addr2, amt, vAccStartTime, vAccEndTime) + accountMapper.SetAccount(ctx, &vacc) + + // require vesting account can pay for fees with their spendable balance + _, err = bankKeeper.DeductFees(ctx, addr2, fee) + require.NoError(t, err, "unexpected fee deduction failure") + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(4))}, + bankKeeper.GetCoins(ctx, addr2), "expected account balance to reflect fee deduction", + ) + + // require transferred coins are equal to the fee as the spendable coins are + // greater + acc, ok := accountMapper.GetAccount(ctx, addr2).(*auth.ContinuousVestingAccount) + require.True(t, ok) + require.Equal(t, fee, acc.TransferredCoins) + + vAccStartTime = header.Time.Add(-1 * time.Hour) + vAccEndTime = header.Time.Add(1 * time.Hour) + vAmt := getVestingTotal(coin, header.Time, vAccStartTime, vAccEndTime) + fee = sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(3))} + + vacc = auth.NewContinuousVestingAccount(addr3, amt, vAccStartTime, vAccEndTime) + accountMapper.SetAccount(ctx, &vacc) + + // require vesting account can pay for fees with their spendable balance + _, err = bankKeeper.DeductFees(ctx, addr3, fee) + require.NoError(t, err, "unexpected fee deduction failure") + require.Equal( + t, sdk.Coins{sdk.NewCoin(testCoinDenom, sdk.NewInt(2))}, + bankKeeper.GetCoins(ctx, addr3), "expected account balance to reflect fee deduction", + ) + + // require transferred coins are equal to the spendable coins as they are less + // than the fee + acc, ok = accountMapper.GetAccount(ctx, addr3).(*auth.ContinuousVestingAccount) + require.True(t, ok) + require.Equal(t, sdk.Coins{sdk.NewCoin(testCoinDenom, vAmt)}, acc.TransferredCoins) +} diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index cc46646a730e..669bb372e1ab 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -247,7 +247,7 @@ func (k Keeper) RemoveRedelegation(ctx sdk.Context, red types.Redelegation) { func (k Keeper) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Coin, validator types.Validator, subtractAccount bool) (newShares sdk.Dec, err sdk.Error) { - // Get or create the delegator delegation + // get or create the delegator delegation delegation, found := k.GetDelegation(ctx, delAddr, validator.OperatorAddr) if !found { delegation = types.Delegation{ @@ -258,8 +258,7 @@ func (k Keeper) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Co } if subtractAccount { - // Account new shares, save - _, _, err = k.bankKeeper.SubtractCoins(ctx, delegation.DelegatorAddr, sdk.Coins{bondAmt}) + _, err = k.bankKeeper.DelegateCoins(ctx, delegation.DelegatorAddr, sdk.Coins{bondAmt}) if err != nil { return } @@ -269,7 +268,7 @@ func (k Keeper) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Co validator, pool, newShares = validator.AddTokensFromDel(pool, bondAmt.Amount) delegation.Shares = delegation.Shares.Add(newShares) - // Update delegation height + // update delegation height delegation.Height = ctx.BlockHeight() k.SetPool(ctx, pool)