diff --git a/x/validator-vesting/abci.go b/x/validator-vesting/abci.go new file mode 100644 index 00000000..c38dcaab --- /dev/null +++ b/x/validator-vesting/abci.go @@ -0,0 +1,73 @@ +package validatorvesting + +import ( + "bytes" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper" + abci "github.com/tendermint/tendermint/abci/types" +) + +// BeginBlocker updates the vote signing information for each validator vesting account, updates account when period changes, and updates the previousBlockTime value in the store. +func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) { + previousBlockTime := time.Time{} + if ctx.BlockHeight() > 1 { + previousBlockTime = k.GetPreviousBlockTime(ctx) + } + + currentBlockTime := req.Header.GetTime() + var voteInfos VoteInfos + voteInfos = ctx.VoteInfos() + validatorVestingKeys := k.GetAllAccountKeys(ctx) + for _, key := range validatorVestingKeys { + acc := k.GetAccountFromAuthKeeper(ctx, key) + if voteInfos.ContainsValidatorAddress(acc.ValidatorAddress) { + vote := voteInfos.MustFilterByValidatorAddress(acc.ValidatorAddress) + if !vote.SignedLastBlock { + // if the validator explicitly missed signing the block, increment the missing sign count + k.UpdateMissingSignCount(ctx, acc.GetAddress(), true) + } else { + k.UpdateMissingSignCount(ctx, acc.GetAddress(), false) + } + } else { + // if the validator was not a voting member of the validator set, increment the missing sign count + k.UpdateMissingSignCount(ctx, acc.GetAddress(), true) + } + + // check if a period ended in the last block + endTimes := k.GetPeriodEndTimes(ctx, key) + for i, t := range endTimes { + if currentBlockTime.Unix() >= t && previousBlockTime.Unix() < t { + k.UpdateVestedCoinsProgress(ctx, key, i) + } + k.HandleVestingDebt(ctx, key, currentBlockTime) + } + } + k.SetPreviousBlockTime(ctx, currentBlockTime) +} + +// VoteInfos an array of abci.VoteInfo +type VoteInfos []abci.VoteInfo + +// ContainsValidatorAddress returns true if the input validator address is found in the VoteInfos array +func (vis VoteInfos) ContainsValidatorAddress(consAddress sdk.ConsAddress) bool { + for _, vi := range vis { + votingAddress := sdk.ConsAddress(vi.Validator.Address) + if bytes.Equal(consAddress, votingAddress) { + return true + } + } + return false +} + +// MustFilterByValidatorAddress returns the VoteInfo that has a validator address matching the input validator address +func (vis VoteInfos) MustFilterByValidatorAddress(consAddress sdk.ConsAddress) abci.VoteInfo { + for i, vi := range vis { + votingAddress := sdk.ConsAddress(vi.Validator.Address) + if bytes.Equal(consAddress, votingAddress) { + return vis[i] + } + } + panic("validator address not found") +} diff --git a/x/validator-vesting/alias.go b/x/validator-vesting/alias.go new file mode 100644 index 00000000..84e2aef0 --- /dev/null +++ b/x/validator-vesting/alias.go @@ -0,0 +1,22 @@ +// nolint +package validatorvesting + +import ( + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey +) + +var ( + NewGenesisState = types.NewGenesisState +) + +type ( + GenesisState = types.GenesisState + Keeper = keeper.Keeper + ValidatorVestingAccount = types.ValidatorVestingAccount +) diff --git a/x/validator-vesting/genesis.go b/x/validator-vesting/genesis.go new file mode 100644 index 00000000..b21fa3c1 --- /dev/null +++ b/x/validator-vesting/genesis.go @@ -0,0 +1,19 @@ +package validatorvesting + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types" +) + +// InitGenesis stores the account address of each ValidatorVestingAccount in the validator vesting keeper, for faster lookup. +// CONTRACT: Accounts created by the account keeper must have already been initialized/created +func InitGenesis(ctx sdk.Context, keeper Keeper, accountKeeper types.AccountKeeper, data GenesisState) { + data.Accounts = auth.SanitizeGenesisAccounts(data.Accounts) + for _, a := range data.Accounts { + vv, ok := a.(ValidatorVestingAccount) + if ok { + keeper.SetValidatorVestingAccountKey(ctx, vv.Address) + } + } +} diff --git a/x/validator-vesting/internal/keeper/keeper.go b/x/validator-vesting/internal/keeper/keeper.go new file mode 100644 index 00000000..b30c14ce --- /dev/null +++ b/x/validator-vesting/internal/keeper/keeper.go @@ -0,0 +1,188 @@ +package keeper + +import ( + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types" + "github.com/tendermint/tendermint/libs/log" +) + +var ( + // BlocktimeKey key for the time of the previous block + BlocktimeKey = []byte{0x00} +) + +// Keeper of the validatorvesting store +type Keeper struct { + storeKey sdk.StoreKey + cdc *codec.Codec + ak types.AccountKeeper + bk types.BankKeeper + supplyKeeper types.SupplyKeeper + stakingKeeper types.StakingKeeper +} + +// NewKeeper creates a new Keeper instance +func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, ak types.AccountKeeper, bk types.BankKeeper, sk types.SupplyKeeper, stk types.StakingKeeper) Keeper { + + return Keeper{ + cdc: cdc, + storeKey: key, + ak: ak, + bk: bk, + supplyKeeper: sk, + stakingKeeper: stk, + } +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} + +// GetPreviousBlockTime get the blocktime for the previous block +func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time) { + store := ctx.KVStore(k.storeKey) + b := store.Get(BlocktimeKey) + if b == nil { + panic("Previous block time not set") + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime) + return blockTime +} + +// SetPreviousBlockTime set the time of the previous block +func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(blockTime) + store.Set(BlocktimeKey, b) +} + +// UpdateMissingSignCount increments the count of blocks missed during the current period +func (k Keeper) UpdateMissingSignCount(ctx sdk.Context, addr sdk.AccAddress, missedBlock bool) { + vv := k.GetAccountFromAuthKeeper(ctx, addr) + if missedBlock { + vv.MissingSignCount[0]++ + } + vv.MissingSignCount[1]++ + k.ak.SetAccount(ctx, vv) +} + +// UpdateVestedCoinsProgress sets the VestingPeriodProgress variable (0 = coins did not vest for the period, 1 = coins did vest for the period) for the given address and period. If coins did not vest, those coins are added to DebtAfterFailedVesting. Finally, MissingSignCount is reset to [0,0], representing that the next period has started and no blocks have been missed. +func (k Keeper) UpdateVestedCoinsProgress(ctx sdk.Context, addr sdk.AccAddress, period int) { + vv := k.GetAccountFromAuthKeeper(ctx, addr) + + threshold := sdk.NewDec(vv.SigningThreshold) + blocksMissed := sdk.NewDec(vv.MissingSignCount[0]) + blockCount := sdk.NewDec(vv.MissingSignCount[1]) + blocksSigned := blockCount.Sub(blocksMissed) + percentageBlocksSigned := blocksSigned.Quo(blockCount).Mul(sdk.NewDec(100)) + successfulVest := percentageBlocksSigned.GTE(threshold) + if successfulVest { + vv.VestingPeriodProgress[period] = 1 + } else { + vv.VestingPeriodProgress[period] = 0 + notVestedTokens := vv.VestingPeriods[period].VestingAmount + // add the tokens that did not vest to DebtAfterFailedVesting + vv.DebtAfterFailedVesting = vv.DebtAfterFailedVesting.Add(notVestedTokens) + } + // reset the number of missed blocks and total number of blocks in the period to zero + vv.MissingSignCount = []int64{0, 0} + k.ak.SetAccount(ctx, vv) +} + +// HandleVestingDebt removes coins after a vesting period in which the vesting +// threshold was not met. Sends/Burns tokens if there is enough spendable tokens, +// otherwise unbonds all existing tokens. +func (k Keeper) HandleVestingDebt(ctx sdk.Context, addr sdk.AccAddress, blockTime time.Time) { + vv := k.GetAccountFromAuthKeeper(ctx, addr) + remainingDebt := !vv.DebtAfterFailedVesting.IsZero() + if remainingDebt { + spendableCoins := vv.SpendableCoins(blockTime) + if spendableCoins.IsAllGTE(vv.DebtAfterFailedVesting) { + if vv.ReturnAddress != nil { + err := k.bk.SendCoins(ctx, addr, vv.ReturnAddress, vv.DebtAfterFailedVesting) + if err != nil { + panic(err) + } + } else { + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, vv.DebtAfterFailedVesting) + if err != nil { + panic(err) + } + err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, vv.DebtAfterFailedVesting) + if err != nil { + panic(err) + } + } + vv.DebtAfterFailedVesting = sdk.NewCoins() + k.ak.SetAccount(ctx, vv) + } else { + // iterate over all delegations made from the validator vesting account and undelegate + // note that we cannot safely undelegate only an amount of shares that covers the debt, + // because the value of those shares could change if a validator gets slashed + k.stakingKeeper.IterateDelegations(ctx, vv.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + k.stakingKeeper.Undelegate(ctx, d.GetDelegatorAddr(), d.GetValidatorAddr(), d.GetShares()) + return false + }) + } + } +} + +// SetValidatorVestingAccountKey stores the account key in the store. This is useful for when we want to iterate over all ValidatorVestingAcounts, so we can avoid iterating over any other accounts stored in the auth keeper. +func (k Keeper) SetValidatorVestingAccountKey(ctx sdk.Context, addr sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + // using empty bytes as value since the only thing we want to do is iterate over the keys. + store.Set(types.ValidatorVestingAccountKey(addr), []byte{0}) +} + +// IterateAccountKeys iterates over all the stored account keys and performs a callback function +func (k Keeper) IterateAccountKeys(ctx sdk.Context, cb func(accountKey []byte) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iterator := sdk.KVStorePrefixIterator(store, types.ValidatorVestingAccountPrefix) + + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + accountKey := iterator.Key() + + if cb(accountKey) { + break + } + } +} + +// GetAllAccountKeys returns all account keys in the validator vesting keeper. +func (k Keeper) GetAllAccountKeys(ctx sdk.Context) (keys [][]byte) { + k.IterateAccountKeys(ctx, + func(key []byte) (stop bool) { + keys = append(keys, key) + return false + }) + return keys +} + +// GetAccountFromAuthKeeper returns a ValidatorVestingAccount from the auth keeper +func (k Keeper) GetAccountFromAuthKeeper(ctx sdk.Context, addr sdk.AccAddress) *types.ValidatorVestingAccount { + acc := k.ak.GetAccount(ctx, addr) + vv, ok := acc.(*types.ValidatorVestingAccount) + if ok { + return vv + } + panic("validator vesting account not found") +} + +// GetPeriodEndTimes returns an array of the times when each period ends +func (k Keeper) GetPeriodEndTimes(ctx sdk.Context, addr sdk.AccAddress) []int64 { + var endTimes []int64 + vv := k.GetAccountFromAuthKeeper(ctx, addr) + currentEndTime := vv.StartTime + for _, p := range vv.VestingPeriods { + currentEndTime += p.PeriodLength + endTimes = append(endTimes, currentEndTime) + } + return endTimes +} diff --git a/x/validator-vesting/internal/keeper/keeper_test.go b/x/validator-vesting/internal/keeper/keeper_test.go new file mode 100644 index 00000000..fe2bee7e --- /dev/null +++ b/x/validator-vesting/internal/keeper/keeper_test.go @@ -0,0 +1,282 @@ +package keeper + +// TODO +// 1. Test that a signed block by the ValidatorAddress increases the blocks counter, but not the missed blocks counter +// 2. Test that an unsigned block increass both the blocks counter and the missed blocks counter +// 3. Test that the previous block time gets updated at the end of each begin block +// 4. Test that the block before a pivital block doesn't reset the period +// 5. Test that a pivotal block results in coins being vested successfully if the treshold is met +// 6. Test that a pivotal block results in HandleVestingDebt + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + tmtime "github.com/tendermint/tendermint/types/time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" +) + +func TestGetSetValidatorVestingAccounts(t *testing.T) { + ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000) + + vva := ValidatorVestingTestAccount() + // Add the validator vesting account to the auth store + ak.SetAccount(ctx, vva) + + // require that the keeper can set the account key without panic + require.NotPanics(t, func() { keeper.SetValidatorVestingAccountKey(ctx, vva.Address) }) + + // require that we can get the account from auth keeper as a validator vesting account. + require.NotPanics(t, func() { keeper.GetAccountFromAuthKeeper(ctx, vva.Address) }) + + // fetching a regular account from the auth keeper does not panic + require.NotPanics(t, func() { ak.GetAccount(ctx, TestAddrs[0]) }) + + // fetching a regular account from the validator vesting keeper panics. + require.Panics(t, func() { keeper.GetAccountFromAuthKeeper(ctx, TestAddrs[0]) }) + + // require that GetAllAccountKeys returns one account + keys := keeper.GetAllAccountKeys(ctx) + require.Equal(t, 1, len(keys)) +} + +func TestGetSetPreviousBlock(t *testing.T) { + ctx, _, _, _, _, keeper := CreateTestInput(t, false, 1000) + now := tmtime.Now() + + // require panic if the previous blocktime was never set + require.Panics(t, func() { keeper.GetPreviousBlockTime(ctx) }) + + // require that passing a valid time to SetPreviousBlockTime does not panic + require.NotPanics(t, func() { keeper.SetPreviousBlockTime(ctx, now) }) + + // require that the value from GetPreviousBlockTime equals what was set + bpt := keeper.GetPreviousBlockTime(ctx) + require.Equal(t, now, bpt) + +} + +func TestSetMissingSignCount(t *testing.T) { + ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000) + + vva := ValidatorVestingTestAccount() + // Add the validator vesting account to the auth store + ak.SetAccount(ctx, vva) + + // require empty array after ValidatorVestingAccount is initialized + require.Equal(t, []int64{0, 0}, vva.MissingSignCount) + + // validator signs a block + keeper.UpdateMissingSignCount(ctx, vva.Address, false) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + require.Equal(t, []int64{0, 1}, vva.MissingSignCount) + + // validator misses a block + keeper.UpdateMissingSignCount(ctx, vva.Address, true) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + require.Equal(t, []int64{1, 2}, vva.MissingSignCount) + +} + +func TestUpdateVestedCoinsProgress(t *testing.T) { + ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000) + + vva := ValidatorVestingTestAccount() + + // Add the validator vesting account to the auth store + ak.SetAccount(ctx, vva) + + // require all vesting period tracking variables to be zero after validator vesting account is initialized + require.Equal(t, []int{0, 0, 0}, vva.VestingPeriodProgress) + + // period 0 passes with all blocks signed + vva.MissingSignCount[0] = 0 + vva.MissingSignCount[1] = 100 + ak.SetAccount(ctx, vva) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that debt is zero + require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting) + // require that the first vesting progress variable is 1 + require.Equal(t, []int{1, 0, 0}, vva.VestingPeriodProgress) + + // require that the missing block counter has reset + require.Equal(t, []int64{0, 0}, vva.MissingSignCount) + + vva = ValidatorVestingTestAccount() + // Add the validator vesting account to the auth store + ak.SetAccount(ctx, vva) + // period 0 passes with 50% of blocks signed (below threshold) + vva.MissingSignCount[0] = 50 + vva.MissingSignCount[1] = 100 + ak.SetAccount(ctx, vva) + keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that period 1 coins have become debt + require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)), vva.DebtAfterFailedVesting) + // require that the first vesting progress variable is 0 + require.Equal(t, []int{0, 0, 0}, vva.VestingPeriodProgress) + // require that the missing block counter has reset + require.Equal(t, []int64{0, 0}, vva.MissingSignCount) +} + +func TestHandleVestingDebtForcedUnbond(t *testing.T) { + ctx, ak, _, stakingKeeper, _, keeper := CreateTestInput(t, false, 1000) + + vva := ValidatorVestingTestAccount() + // Delegate all coins + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + now := tmtime.Now() + vva.TrackDelegation(now, origCoins) + + // Add the validator vesting account to the auth store + ak.SetAccount(ctx, vva) + + // Require that calling HandleVestingDebt when debt is zero doesn't alter the delegation + keeper.HandleVestingDebt(ctx, vva.Address, now) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + require.Equal(t, origCoins, vva.DelegatedVesting) + require.Nil(t, vva.DelegatedFree) + require.Nil(t, vva.GetCoins()) + + // Create validators and a delegation from the validator vesting account + CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) + + vva = ValidatorVestingDelegatorTestAccount(now) + ak.SetAccount(ctx, vva) + delTokens := sdk.TokensFromConsensusPower(30) + val1, found := stakingKeeper.GetValidator(ctx, valOpAddr1) + require.True(t, found) + + _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, stakingKeeper) + + // require that there exists one delegation + var delegations int + stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + delegations++ + return false + }) + + require.Equal(t, 1, delegations) + + // period 0 passes and the threshold is not met + vva.MissingSignCount[0] = 50 + vva.MissingSignCount[1] = 100 + ak.SetAccount(ctx, vva) + keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that period 0 coins have become debt + require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 30000000)), vva.DebtAfterFailedVesting) + + // when there are no additional liquid coins in the account, require that there are no delegations after HandleVestingDebt (ie the account has been force unbonded) + keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour)) + // _ = staking.EndBlocker(ctx, stakingKeeper) + delegations = 0 + stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + delegations++ + return false + }) + require.Equal(t, 0, delegations) + +} + +func TestHandleVestingDebtBurn(t *testing.T) { + ctx, ak, _, stakingKeeper, supplyKeeper, keeper := CreateTestInput(t, false, 1000) + CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) + now := tmtime.Now() + vva := ValidatorVestingDelegatorTestAccount(now) + ak.SetAccount(ctx, vva) + delTokens := sdk.TokensFromConsensusPower(30) + val1, found := stakingKeeper.GetValidator(ctx, valOpAddr1) + require.True(t, found) + _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, stakingKeeper) + + // receive some coins so that the debt will be covered by liquid balance + recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)} + vva.SetCoins(vva.GetCoins().Add(recvAmt)) + + // period 0 passes and the threshold is not met + vva.MissingSignCount[0] = 50 + vva.MissingSignCount[1] = 100 + ak.SetAccount(ctx, vva) + keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that period 0 coins have become debt + require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 30000000)), vva.DebtAfterFailedVesting) + + initialSupply := supplyKeeper.GetSupply(ctx).GetTotal() + expectedSupply := initialSupply.Sub(vva.DebtAfterFailedVesting) + // Context needs the block time because bank keeper calls 'SpendableCoins' by getting the header from the context. + ctx = ctx.WithBlockTime(now.Add(12 * time.Hour)) + keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour)) + // in the case when the return address is not set require that the total supply has decreased by the debt amount + require.Equal(t, expectedSupply, supplyKeeper.GetSupply(ctx).GetTotal()) + // require that there is still one delegation + delegations := 0 + stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + delegations++ + return false + }) + require.Equal(t, 1, delegations) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + //require that debt is now zero + require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting) +} + +func TestHandleVestingDebtReturn(t *testing.T) { + ctx, ak, _, stakingKeeper, _, keeper := CreateTestInput(t, false, 1000) + CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) + now := tmtime.Now() + vva := ValidatorVestingDelegatorTestAccount(now) + vva.ReturnAddress = TestAddrs[2] + ak.SetAccount(ctx, vva) + delTokens := sdk.TokensFromConsensusPower(30) + val1, found := stakingKeeper.GetValidator(ctx, valOpAddr1) + require.True(t, found) + _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, stakingKeeper) + + // receive some coins so that the debt will be covered by liquid balance + recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)} + vva.SetCoins(vva.GetCoins().Add(recvAmt)) + + // period 0 passes and the threshold is not met + vva.MissingSignCount[0] = 50 + vva.MissingSignCount[1] = 100 + ak.SetAccount(ctx, vva) + keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that period 0 coins have become debt + require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 30000000)), vva.DebtAfterFailedVesting) + + initialBalance := ak.GetAccount(ctx, TestAddrs[2]).GetCoins() + expectedBalance := initialBalance.Add(vva.DebtAfterFailedVesting) + // Context needs the block time because bank keeper calls 'SpendableCoins' by getting the header from the context. + ctx = ctx.WithBlockTime(now.Add(12 * time.Hour)) + keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour)) + // in the case when the return address is, set require that return address balance has increased by the debt amount + require.Equal(t, expectedBalance, ak.GetAccount(ctx, TestAddrs[2]).GetCoins()) + // require that there is still one delegation + delegations := 0 + stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + delegations++ + return false + }) + require.Equal(t, 1, delegations) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + //require that debt is now zero + require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting) +} diff --git a/x/validator-vesting/internal/keeper/test_common.go b/x/validator-vesting/internal/keeper/test_common.go new file mode 100644 index 00000000..a3ed9945 --- /dev/null +++ b/x/validator-vesting/internal/keeper/test_common.go @@ -0,0 +1,220 @@ +package keeper + +// nolint:deadcode unused +// DONTCOVER +// noalias +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/libs/log" + tmtime "github.com/tendermint/tendermint/types/time" + dbm "github.com/tendermint/tm-db" + + "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" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types" +) + +//nolint: deadcode unused +var ( + delPk1 = ed25519.GenPrivKey().PubKey() + delPk2 = ed25519.GenPrivKey().PubKey() + delPk3 = ed25519.GenPrivKey().PubKey() + delAddr1 = sdk.AccAddress(delPk1.Address()) + delAddr2 = sdk.AccAddress(delPk2.Address()) + delAddr3 = sdk.AccAddress(delPk3.Address()) + + valOpPk1 = ed25519.GenPrivKey().PubKey() + valOpPk2 = ed25519.GenPrivKey().PubKey() + valOpPk3 = ed25519.GenPrivKey().PubKey() + valOpAddr1 = sdk.ValAddress(valOpPk1.Address()) + valOpAddr2 = sdk.ValAddress(valOpPk2.Address()) + valOpAddr3 = sdk.ValAddress(valOpPk3.Address()) + valAccAddr1 = sdk.AccAddress(valOpPk1.Address()) // generate acc addresses for these validator keys too + valAccAddr2 = sdk.AccAddress(valOpPk2.Address()) + valAccAddr3 = sdk.AccAddress(valOpPk3.Address()) + + valConsPk1 = ed25519.GenPrivKey().PubKey() + valConsPk2 = ed25519.GenPrivKey().PubKey() + valConsPk3 = ed25519.GenPrivKey().PubKey() + valConsAddr1 = sdk.ConsAddress(valConsPk1.Address()) + valConsAddr2 = sdk.ConsAddress(valConsPk2.Address()) + valConsAddr3 = sdk.ConsAddress(valConsPk3.Address()) + + // TODO move to common testing package for all modules + // test addresses + TestAddrs = []sdk.AccAddress{ + delAddr1, delAddr2, delAddr3, + valAccAddr1, valAccAddr2, valAccAddr3, + } + + emptyDelAddr sdk.AccAddress + emptyValAddr sdk.ValAddress + emptyPubkey crypto.PubKey + stakeDenom = "stake" + feeDenom = "fee" +) + +func MakeTestCodec() *codec.Codec { + var cdc = codec.New() + auth.RegisterCodec(cdc) + vesting.RegisterCodec(cdc) + types.RegisterCodec(cdc) + supply.RegisterCodec(cdc) + staking.RegisterCodec(cdc) + sdk.RegisterCodec(cdc) + codec.RegisterCrypto(cdc) + + return cdc +} + +// test common should produce a staking keeper, a supply keeper, a bank keeper, an auth keeper, a validatorvesting keeper, a context, + +func CreateTestInput(t *testing.T, isCheckTx bool, initPower int64) (sdk.Context, auth.AccountKeeper, bank.Keeper, staking.Keeper, supply.Keeper, Keeper) { + + initTokens := sdk.TokensFromConsensusPower(initPower) + + keyAcc := sdk.NewKVStoreKey(auth.StoreKey) + keyStaking := sdk.NewKVStoreKey(staking.StoreKey) + keySupply := sdk.NewKVStoreKey(supply.StoreKey) + keyParams := sdk.NewKVStoreKey(params.StoreKey) + tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) + keyValidatorVesting := sdk.NewKVStoreKey(types.StoreKey) + + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + + ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyValidatorVesting, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) + require.Nil(t, ms.LoadLatestVersion()) + + ctx := sdk.NewContext(ms, abci.Header{ChainID: "foo-chain"}, isCheckTx, log.NewNopLogger()) + + feeCollectorAcc := supply.NewEmptyModuleAccount(auth.FeeCollectorName) + notBondedPool := supply.NewEmptyModuleAccount(staking.NotBondedPoolName, supply.Burner, supply.Staking) + bondPool := supply.NewEmptyModuleAccount(staking.BondedPoolName, supply.Burner, supply.Staking) + validatorVestingAcc := supply.NewEmptyModuleAccount(types.ModuleName) + + blacklistedAddrs := make(map[string]bool) + blacklistedAddrs[feeCollectorAcc.GetAddress().String()] = true + blacklistedAddrs[notBondedPool.GetAddress().String()] = true + blacklistedAddrs[bondPool.GetAddress().String()] = true + blacklistedAddrs[validatorVestingAcc.GetAddress().String()] = true + + cdc := MakeTestCodec() + + pk := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace) + + accountKeeper := auth.NewAccountKeeper(cdc, keyAcc, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount) + bankKeeper := bank.NewBaseKeeper(accountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs) + maccPerms := map[string][]string{ + auth.FeeCollectorName: nil, + staking.NotBondedPoolName: {supply.Burner, supply.Staking}, + staking.BondedPoolName: {supply.Burner, supply.Staking}, + types.ModuleName: {supply.Burner}, + } + supplyKeeper := supply.NewKeeper(cdc, keySupply, accountKeeper, bankKeeper, maccPerms) + + stakingKeeper := staking.NewKeeper(cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace) + stakingKeeper.SetParams(ctx, staking.DefaultParams()) + + keeper := NewKeeper(cdc, keyValidatorVesting, accountKeeper, bankKeeper, supplyKeeper, stakingKeeper) + + initCoins := sdk.NewCoins(sdk.NewCoin(stakingKeeper.BondDenom(ctx), initTokens)) + totalSupply := sdk.NewCoins(sdk.NewCoin(stakingKeeper.BondDenom(ctx), initTokens.MulRaw(int64(len(TestAddrs))))) + supplyKeeper.SetSupply(ctx, supply.NewSupply(totalSupply)) + + // fill all the addresses with some coins, set the loose pool tokens simultaneously + for _, addr := range TestAddrs { + _, err := bankKeeper.AddCoins(ctx, addr, initCoins) + require.Nil(t, err) + } + + // set module accounts + keeper.supplyKeeper.SetModuleAccount(ctx, feeCollectorAcc) + keeper.supplyKeeper.SetModuleAccount(ctx, notBondedPool) + keeper.supplyKeeper.SetModuleAccount(ctx, bondPool) + + return ctx, accountKeeper, bankKeeper, stakingKeeper, supplyKeeper, keeper +} + +func ValidatorVestingTestAccount() *types.ValidatorVestingAccount { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := types.CreateTestAddrs(1)[0] + testPk := types.CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := types.NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + err := vva.Validate() + if err != nil { + panic(err) + } + return vva +} + +func ValidatorVestingDelegatorTestAccount(startTime time.Time) *types.ValidatorVestingAccount { + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 15000000)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 15000000)}}, + } + testAddr := types.CreateTestAddrs(1)[0] + testPk := types.CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 60000000)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := types.NewValidatorVestingAccount(&bacc, startTime.Unix(), periods, testConsAddr, nil, 90) + err := vva.Validate() + if err != nil { + panic(err) + } + return vva +} + +func CreateValidators(ctx sdk.Context, sk staking.Keeper, powers []int64) { + val1 := staking.NewValidator(valOpAddr1, valOpPk1, staking.Description{}) + val2 := staking.NewValidator(valOpAddr2, valOpPk2, staking.Description{}) + val3 := staking.NewValidator(valOpAddr3, valOpPk3, staking.Description{}) + + sk.SetValidator(ctx, val1) + sk.SetValidator(ctx, val2) + sk.SetValidator(ctx, val3) + sk.SetValidatorByConsAddr(ctx, val1) + sk.SetValidatorByConsAddr(ctx, val2) + sk.SetValidatorByConsAddr(ctx, val3) + sk.SetNewValidatorByPowerIndex(ctx, val1) + sk.SetNewValidatorByPowerIndex(ctx, val2) + sk.SetNewValidatorByPowerIndex(ctx, val3) + + _, _ = sk.Delegate(ctx, valAccAddr1, sdk.TokensFromConsensusPower(powers[0]), sdk.Unbonded, val1, true) + _, _ = sk.Delegate(ctx, valAccAddr2, sdk.TokensFromConsensusPower(powers[1]), sdk.Unbonded, val2, true) + _, _ = sk.Delegate(ctx, valAccAddr3, sdk.TokensFromConsensusPower(powers[2]), sdk.Unbonded, val3, true) + + _ = staking.EndBlocker(ctx, sk) +} diff --git a/x/validator-vesting/internal/types/codec.go b/x/validator-vesting/internal/types/codec.go new file mode 100644 index 00000000..1f0fa25f --- /dev/null +++ b/x/validator-vesting/internal/types/codec.go @@ -0,0 +1,19 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" +) + +// RegisterCodec registers concrete types on the codec +func RegisterCodec(cdc *codec.Codec) { + cdc.RegisterConcrete(&ValidatorVestingAccount{}, "cosmos-sdk/ValidatorVestingAccount", nil) +} + +// ModuleCdc module wide codec +var ModuleCdc *codec.Codec + +func init() { + ModuleCdc = codec.New() + RegisterCodec(ModuleCdc) + ModuleCdc.Seal() +} diff --git a/x/validator-vesting/internal/types/expected_keepers.go b/x/validator-vesting/internal/types/expected_keepers.go new file mode 100644 index 00000000..572c767c --- /dev/null +++ b/x/validator-vesting/internal/types/expected_keepers.go @@ -0,0 +1,38 @@ +package types + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/exported" + stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" +) + +// AccountKeeper defines the expected account keeper (noalias) +type AccountKeeper interface { + GetAccount(sdk.Context, sdk.AccAddress) exported.Account + SetAccount(sdk.Context, exported.Account) +} + +// BankKeeper defines the expected bank keeper (noalias) +type BankKeeper interface { + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) sdk.Error +} + +// StakingKeeper defines the expected staking keeper (noalias) +type StakingKeeper interface { + IterateDelegations(ctx sdk.Context, delegator sdk.AccAddress, + fn func(index int64, delegation stakingexported.DelegationI) (stop bool)) + Undelegate( + ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec, + ) (time.Time, sdk.Error) + +} + +// SupplyKeeper defines the expected supply keeper for module accounts (noalias) +type SupplyKeeper interface { + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error + BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error + SetModuleAccount(sdk.Context, supplyexported.ModuleAccountI) +} diff --git a/x/validator-vesting/internal/types/genesis.go b/x/validator-vesting/internal/types/genesis.go new file mode 100644 index 00000000..60fd9a05 --- /dev/null +++ b/x/validator-vesting/internal/types/genesis.go @@ -0,0 +1,22 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/x/auth/exported" +) + +// GenesisState - all auth state that must be provided at genesis +type GenesisState struct { + Accounts exported.GenesisAccounts `json:"accounts" yaml:"accounts"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(accounts exported.GenesisAccounts) GenesisState { + return GenesisState{ + Accounts: accounts, + } +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(exported.GenesisAccounts{}) +} diff --git a/x/validator-vesting/internal/types/key.go b/x/validator-vesting/internal/types/key.go new file mode 100644 index 00000000..a86c1aea --- /dev/null +++ b/x/validator-vesting/internal/types/key.go @@ -0,0 +1,23 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // ModuleName name used throughout module + ModuleName = "validatorvesting" + + // StoreKey to be used when creating the KVStore + StoreKey = ModuleName +) + +var ( + // ValidatorVestingAccountPrefix store prefix for validator vesting accounts + ValidatorVestingAccountPrefix = []byte{0x01} +) + +// ValidatorVestingAccountKey returns the account address bytes prefixed by ValidatorVestingAccountPrefix +func ValidatorVestingAccountKey(addr sdk.AccAddress) []byte { + return append(ValidatorVestingAccountPrefix, addr.Bytes()...) +} \ No newline at end of file diff --git a/x/validator-vesting/internal/types/test_common.go b/x/validator-vesting/internal/types/test_common.go new file mode 100644 index 00000000..6e6091ba --- /dev/null +++ b/x/validator-vesting/internal/types/test_common.go @@ -0,0 +1,82 @@ +package types // noalias + +import ( + "bytes" + "encoding/hex" + "strconv" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// nolint: unparam +func CreateTestAddrs(numAddrs int) []sdk.AccAddress { + var addresses []sdk.AccAddress + var buffer bytes.Buffer + + // start at 100 so we can make up to 999 test addresses with valid test addresses + for i := 100; i < (numAddrs + 100); i++ { + numString := strconv.Itoa(i) + buffer.WriteString("A58856F0FD53BF058B4909A21AEC019107BA6") //base address string + + buffer.WriteString(numString) //adding on final two digits to make addresses unique + res, _ := sdk.AccAddressFromHex(buffer.String()) + bech := res.String() + addresses = append(addresses, TestAddr(buffer.String(), bech)) + buffer.Reset() + } + return addresses +} + +// TestAddr for incode address generation +func TestAddr(addr string, bech string) sdk.AccAddress { + + res, err := sdk.AccAddressFromHex(addr) + if err != nil { + panic(err) + } + bechexpected := res.String() + if bech != bechexpected { + panic("Bech encoding doesn't match reference") + } + + bechres, err := sdk.AccAddressFromBech32(bech) + if err != nil { + panic(err) + } + if !bytes.Equal(bechres, res) { + panic("Bech decode and hex decode don't match") + } + + return res +} + +// nolint: unparam +func CreateTestPubKeys(numPubKeys int) []crypto.PubKey { + var publicKeys []crypto.PubKey + var buffer bytes.Buffer + + //start at 10 to avoid changing 1 to 01, 2 to 02, etc + for i := 100; i < (numPubKeys + 100); i++ { + numString := strconv.Itoa(i) + buffer.WriteString("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AF") //base pubkey string + buffer.WriteString(numString) //adding on final two digits to make pubkeys unique + publicKeys = append(publicKeys, NewPubKey(buffer.String())) + buffer.Reset() + } + return publicKeys +} + +// NewPubKey for incode pubkey generation +func NewPubKey(pk string) (res crypto.PubKey) { + pkBytes, err := hex.DecodeString(pk) + if err != nil { + panic(err) + } + //res, err = crypto.PubKeyFromBytes(pkBytes) + var pkEd ed25519.PubKeyEd25519 + copy(pkEd[:], pkBytes[:]) + return pkEd +} diff --git a/x/validator-vesting/internal/types/validator_vesting_account.go b/x/validator-vesting/internal/types/validator_vesting_account.go new file mode 100644 index 00000000..c02c5350 --- /dev/null +++ b/x/validator-vesting/internal/types/validator_vesting_account.go @@ -0,0 +1,231 @@ +package types + +import ( + "errors" + "time" + + "gopkg.in/yaml.v2" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + vestexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" +) + +// Assert ValidatorVestingAccount implements the vestexported.VestingAccount interface +// Assert +var _ vestexported.VestingAccount = (*ValidatorVestingAccount)(nil) +var _ authexported.GenesisAccount = (*ValidatorVestingAccount)(nil) + +// Register the ValidatorVestingAccount type on the auth module codec +func init() { + auth.RegisterAccountTypeCodec(&ValidatorVestingAccount{}, "cosmos-sdk/ValidatorVestingAccount") +} + +// ValidatorVestingAccount implements the VestingAccount interface. It +// conditionally vests by unlocking coins during each specified period, provided +// that the validator address has validated at least **threshold** blocks during +// the previous vesting period. If the validator has not vested at least the threshold, +// the coins are returned to the return address, or burned if the return address is null. +type ValidatorVestingAccount struct { + *vesting.PeriodicVestingAccount + ValidatorAddress sdk.ConsAddress `json:"validator_address" yaml:"validator_address"` + ReturnAddress sdk.AccAddress `json:"return_address" yaml:"return_address"` + SigningThreshold int64 `json:"signing_threshold" yaml:"signing_threshold"` + MissingSignCount []int64 `json:"missing_sign_count" yaml:"missing_sign_count"` + VestingPeriodProgress []int `json:"vesting_period_progress" yaml:"vesting_period_progress"` + DebtAfterFailedVesting sdk.Coins +} + +// NewValidatorVestingAccountRaw creates a new ValidatorVestingAccount object from BaseVestingAccount +func NewValidatorVestingAccountRaw(bva *vesting.BaseVestingAccount, + startTime int64, periods vesting.VestingPeriods, validatorAddress sdk.ConsAddress, returnAddress sdk.AccAddress, signingThreshold int64) *ValidatorVestingAccount { + cva := &vesting.ContinuousVestingAccount{ + StartTime: startTime, + BaseVestingAccount: bva, + } + pva := &vesting.PeriodicVestingAccount{ + ContinuousVestingAccount: cva, + VestingPeriods: periods, + } + var vestingPeriodProgress = make([]int, len(periods)) + + return &ValidatorVestingAccount{ + PeriodicVestingAccount: pva, + ValidatorAddress: validatorAddress, + ReturnAddress: returnAddress, + SigningThreshold: signingThreshold, + MissingSignCount: []int64{0, 0}, + VestingPeriodProgress: vestingPeriodProgress, + DebtAfterFailedVesting: sdk.NewCoins(), + } +} + +// NewValidatorVestingAccount creates a ValidatorVestingAccount object from a BaseAccount +func NewValidatorVestingAccount(baseAcc *auth.BaseAccount, startTime int64, periods vesting.VestingPeriods, validatorAddress sdk.ConsAddress, returnAddress sdk.AccAddress, signingThreshold int64) *ValidatorVestingAccount { + + endTime := startTime + for _, p := range periods { + endTime += p.PeriodLength + } + baseVestingAcc := &vesting.BaseVestingAccount{ + BaseAccount: baseAcc, + OriginalVesting: baseAcc.Coins, + EndTime: endTime, + } + cva := &vesting.ContinuousVestingAccount{ + StartTime: startTime, + BaseVestingAccount: baseVestingAcc, + } + pva := &vesting.PeriodicVestingAccount{ + ContinuousVestingAccount: cva, + VestingPeriods: periods, + } + var vestingPeriodProgress = make([]int, len(periods)) + + debt := sdk.NewCoins() + + return &ValidatorVestingAccount{ + PeriodicVestingAccount: pva, + ValidatorAddress: validatorAddress, + ReturnAddress: returnAddress, + SigningThreshold: signingThreshold, + MissingSignCount: []int64{0, 0}, + VestingPeriodProgress: vestingPeriodProgress, + DebtAfterFailedVesting: debt, + } +} + +// GetVestedCoins returns the total number of vested coins. +func (vva ValidatorVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins { + var vestedCoins sdk.Coins + if blockTime.Unix() <= vva.StartTime { + return vestedCoins + } + currentPeriodStartTime := vva.StartTime + numberPeriods := len(vva.VestingPeriods) + for i := 0; i < numberPeriods; i++ { + x := blockTime.Unix() - currentPeriodStartTime + if x >= vva.VestingPeriods[i].PeriodLength { + vestedSuccess := vva.VestingPeriodProgress[i] > 0 + if vestedSuccess { + vestedCoins = vestedCoins.Add(vva.VestingPeriods[i].VestingAmount) + } + currentPeriodStartTime += vva.VestingPeriods[i].PeriodLength + } else { + break + } + } + return vestedCoins + +} + +// GetFailedVestedCoins returns the total number of coins for which the vesting period has passed but the vesting threshold was not met. +func (vva ValidatorVestingAccount) GetFailedVestedCoins(blockTime time.Time) sdk.Coins { + var failedVestedCoins sdk.Coins + if blockTime.Unix() <= vva.StartTime { + return failedVestedCoins + } + currentPeriodStartTime := vva.StartTime + numberPeriods := len(vva.VestingPeriods) + for i := 0; i < numberPeriods; i++ { + x := blockTime.Unix() - currentPeriodStartTime + if x >= vva.VestingPeriods[i].PeriodLength { + vestedFailure := vva.VestingPeriodProgress[i] == 0 + if vestedFailure { + failedVestedCoins = failedVestedCoins.Add(vva.VestingPeriods[i].VestingAmount) + } + currentPeriodStartTime += vva.VestingPeriods[i].PeriodLength + } else { + break + } + } + return failedVestedCoins +} + +// GetVestingCoins returns the total number of vesting coins. For validator vesting accounts, this excludes coins for which the vesting period has passed, but the vesting threshold was not met. +func (vva ValidatorVestingAccount) GetVestingCoins(blockTime time.Time) sdk.Coins { + return vva.OriginalVesting.Sub(vva.GetVestedCoins(blockTime)).Sub(vva.GetFailedVestedCoins(blockTime)) +} + +// SpendableCoins returns the total number of spendable coins per denom for a +// periodic vesting account. +func (vva ValidatorVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins { + return vva.BaseVestingAccount.SpendableCoinsFromVestingCoins(vva.GetVestingCoins(blockTime)).Sub(vva.DebtAfterFailedVesting) +} + +// 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 (vva *ValidatorVestingAccount) TrackDelegation(blockTime time.Time, amount sdk.Coins) { + vva.BaseVestingAccount.TrackDelegation(vva.GetVestingCoins(blockTime), amount) +} + +// Validate checks for errors on the account fields +func (vva ValidatorVestingAccount) Validate() error { + if vva.SigningThreshold > 100 || vva.SigningThreshold < 0 { + return errors.New("signing threshold must be between 0 and 100") + } + if vva.ReturnAddress.Equals(vva.Address) { + return errors.New("return address cannot be the same as the account address") + } + return vva.PeriodicVestingAccount.Validate() +} + +// MarshalYAML returns the YAML representation of an account. +func (vva ValidatorVestingAccount) MarshalYAML() (interface{}, error) { + var bs []byte + var err error + var pubkey string + + if vva.PubKey != nil { + pubkey, err = sdk.Bech32ifyAccPub(vva.PubKey) + if err != nil { + return nil, err + } + } + + bs, err = yaml.Marshal(struct { + Address sdk.AccAddress + Coins sdk.Coins + PubKey string + AccountNumber uint64 + Sequence uint64 + OriginalVesting sdk.Coins + DelegatedFree sdk.Coins + DelegatedVesting sdk.Coins + EndTime int64 + StartTime int64 + VestingPeriods vesting.VestingPeriods + ValidatorAddress sdk.ConsAddress + ReturnAddress sdk.AccAddress + SigningThreshold int64 + MissingSignCount []int64 + VestingPeriodProgress []int + DebtAfterFailedVesting sdk.Coins + }{ + Address: vva.Address, + Coins: vva.Coins, + PubKey: pubkey, + AccountNumber: vva.AccountNumber, + Sequence: vva.Sequence, + OriginalVesting: vva.OriginalVesting, + DelegatedFree: vva.DelegatedFree, + DelegatedVesting: vva.DelegatedVesting, + EndTime: vva.EndTime, + StartTime: vva.StartTime, + VestingPeriods: vva.VestingPeriods, + ValidatorAddress: vva.ValidatorAddress, + ReturnAddress: vva.ReturnAddress, + SigningThreshold: vva.SigningThreshold, + MissingSignCount: vva.MissingSignCount, + VestingPeriodProgress: vva.VestingPeriodProgress, + DebtAfterFailedVesting: vva.DebtAfterFailedVesting, + }) + if err != nil { + return nil, err + } + + return string(bs), err +} diff --git a/x/validator-vesting/internal/types/validator_vesting_account_test.go b/x/validator-vesting/internal/types/validator_vesting_account_test.go new file mode 100644 index 00000000..e3e81dd8 --- /dev/null +++ b/x/validator-vesting/internal/types/validator_vesting_account_test.go @@ -0,0 +1,360 @@ +package types + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + tmtime "github.com/tendermint/tendermint/types/time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" +) + +var ( + stakeDenom = "stake" + feeDenom = "fee" +) + +func TestGetVestedCoinsValidatorVestingAcc(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + + // require no coins vested at the beginning of the vesting schedule + vestedCoins := vva.GetVestedCoins(now) + require.Nil(t, vestedCoins) + + // require no coins vested during first vesting period + vestedCoins = vva.GetVestedCoins(now.Add(6 * time.Hour)) + require.Nil(t, vestedCoins) + + // require 50% of coins vested after successful period 1 vesting + vva.VestingPeriodProgress[0] = 1 + vestedCoins = vva.GetVestedCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) + + // require no coins vested after unsuccessful period 1 vesting + vva.VestingPeriodProgress[0] = 0 + vestedCoins = vva.GetVestedCoins(now.Add(12 * time.Hour)) + require.Nil(t, vestedCoins) + + // require period 2 coins don't vest until period is over + vva.VestingPeriodProgress[0] = 1 + // even if the vesting period was somehow successful, should still only return 50% of coins as vested, since the second vesting period hasn't completed. + vva.VestingPeriodProgress[1] = 1 + vestedCoins = vva.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 successful period 2 + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 1 + vestedCoins = vva.GetVestedCoins(now.Add(18 * time.Hour)) + require.Equal(t, + sdk.Coins{ + sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)}, vestedCoins) + + // require 50% of coins vested after successful period 1 and unsuccessful period 2 + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 0 + vestedCoins = vva.GetVestedCoins(now.Add(18 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) + + // require 100% of coins vested after all periods complete successfully + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[2] = 1 + + vestedCoins = vva.GetVestedCoins(now.Add(48 * time.Hour)) + require.Equal(t, origCoins, vestedCoins) +} + +func TestGetVestingCoinsValidatorVestingAcc(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + + // require all coins vesting at the beginning of the vesting schedule + vestingCoins := vva.GetVestingCoins(now) + require.Equal(t, origCoins, vestingCoins) + + // require all coins vesting during first vesting period + vestingCoins = vva.GetVestingCoins(now.Add(6 * time.Hour)) + require.Equal(t, origCoins, vestingCoins) + + // require 50% of coins vesting after successful period 1 vesting + vva.VestingPeriodProgress[0] = 1 + vestingCoins = vva.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 unsuccessful period 1 vesting + vva.VestingPeriodProgress[0] = 0 + vestingCoins = vva.GetVestingCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) + + // require period 2 coins still vesting until period is over + vva.VestingPeriodProgress[0] = 1 + // should never happen, but still won't affect vesting balance + vva.VestingPeriodProgress[1] = 1 + vestingCoins = vva.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 successful period 2 + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 1 + vestingCoins = vva.GetVestingCoins(now.Add(18 * time.Hour)) + require.Equal(t, + sdk.Coins{ + sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins) + + // require 25% of coins vesting after successful period 1 and unsuccessful period 2 + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 0 + vestingCoins = vva.GetVestingCoins(now.Add(18 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins) + + // require no coins vesting after all periods complete successfully + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[2] = 1 + + vestingCoins = vva.GetVestingCoins(now.Add(48 * time.Hour)) + require.Nil(t, vestingCoins) + + // require no coins vesting after all periods complete unsuccessfully + vva.VestingPeriodProgress[0] = 0 + vva.VestingPeriodProgress[1] = 0 + vva.VestingPeriodProgress[2] = 0 + + vestingCoins = vva.GetVestingCoins(now.Add(48 * time.Hour)) + require.Nil(t, vestingCoins) +} + +func TestSpendableCoinsValidatorVestingAccount(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + + // require that there exist no spendable coins at the beginning of the vesting schedule + spendableCoins := vva.SpendableCoins(now) + require.Nil(t, spendableCoins) + + // require that all vested coins (50%) are spendable when period 1 completes successfully + vva.VestingPeriodProgress[0] = 1 + spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, spendableCoins) + + // require that there exist no spendable coins after period 1 completes unsuccessfully. + vva.VestingPeriodProgress[0] = 0 + spendableCoins = vva.SpendableCoins(now) + require.Nil(t, spendableCoins) + + // receive some coins + recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)} + vva.SetCoins(vva.GetCoins().Add(recvAmt)) + + // require that all vested coins (50%) are spendable plus any received after period 1 completes unsuccessfully + vva.VestingPeriodProgress[0] = 1 + spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 100)}, spendableCoins) + + // spend all spendable coins + vva.SetCoins(vva.GetCoins().Sub(spendableCoins)) + + // require that no more coins are spendable + spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour)) + require.Nil(t, spendableCoins) +} + +func TestTrackDelegationValidatorVestingAcc(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + + vva.TrackDelegation(now, origCoins) + require.Equal(t, origCoins, vva.DelegatedVesting) + require.Nil(t, vva.DelegatedFree) + require.Nil(t, vva.GetCoins()) + + // require the ability to delegate all vesting coins (50%) and all vested coins (50%) + bacc.SetCoins(origCoins) + vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedVesting) + require.Nil(t, vva.DelegatedFree) + + vva.VestingPeriodProgress[0] = 1 + vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedVesting) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedFree) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000)}, vva.GetCoins()) + + // require no modifications when delegation amount is zero or not enough funds + bacc.SetCoins(origCoins) + vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + require.Panics(t, func() { + vva.TrackDelegation(now.Add(24*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)}) + }) + require.Nil(t, vva.DelegatedVesting) + require.Nil(t, vva.DelegatedFree) + require.Equal(t, origCoins, vva.GetCoins()) +} + +func TestTrackUndelegationPeriodicVestingAcc(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + + // require ability to delegate then undelegate all coins. + vva.TrackDelegation(now, origCoins) + vva.TrackUndelegation(origCoins) + require.Nil(t, vva.DelegatedFree) + require.Nil(t, vva.DelegatedVesting) + require.Equal(t, origCoins, vva.GetCoins()) + + // require the ability to delegate all coins after they have successfully vested + bacc.SetCoins(origCoins) + vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[2] = 1 + vva.TrackDelegation(now.Add(24*time.Hour), origCoins) + vva.TrackUndelegation(origCoins) + require.Nil(t, vva.DelegatedFree) + require.Nil(t, vva.DelegatedVesting) + require.Equal(t, origCoins, vva.GetCoins()) + + // require panic and no modifications when attempting to undelegate zero coins + bacc.SetCoins(origCoins) + vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + require.Panics(t, func() { + vva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)}) + }) + require.Nil(t, vva.DelegatedFree) + require.Nil(t, vva.DelegatedVesting) + require.Equal(t, origCoins, vva.GetCoins()) + + // successfuly vest period 1 and delegate to two validators + vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + vva.VestingPeriodProgress[0] = 1 + vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + + // undelegate from one validator that got slashed 50% + vva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, vva.DelegatedFree) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedVesting) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 25)}, vva.GetCoins()) + + // undelegate from the other validator that did not get slashed + vva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) + require.Nil(t, vva.DelegatedFree) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, vva.DelegatedVesting) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 75)}, vva.GetCoins()) +} + +func TestGenesisAccountValidate(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + tests := []struct { + name string + acc authexported.GenesisAccount + expErr error + }{ + { + "valid validator vesting account", + NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 100), + nil, + }, + { + "invalid signing threshold", + NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, -1), + errors.New("signing threshold must be between 0 and 100"), + }, + { + "invalid signing threshold", + NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 120), + errors.New("signing threshold must be between 0 and 100"), + }, + { + "invalid return address", + NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, testAddr, 90), + errors.New("return address cannot be the same as the account address"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.acc.Validate() + require.Equal(t, tt.expErr, err) + }) + } +}