diff --git a/types/errors/errors.go b/types/errors/errors.go index 1aa68816ccbf..6c540a1d6149 100644 --- a/types/errors/errors.go +++ b/types/errors/errors.go @@ -135,6 +135,9 @@ var ( // supported. ErrNotSupported = Register(RootCodespace, 37, "feature not supported") + // ErrNotFound defines an error when requested entity doesn't exist in the state. + ErrNotFound = Register(RootCodespace, 38, "not found") + // ErrPanic is only set when we recover from a panic, so we know to // redact potentially sensitive system info ErrPanic = Register(UndefinedCodespace, 111222, "panic") diff --git a/x/auth/keeper/keeper.go b/x/auth/keeper/keeper.go index 92d9f9b76ecb..d596f9138da6 100644 --- a/x/auth/keeper/keeper.go +++ b/x/auth/keeper/keeper.go @@ -202,7 +202,7 @@ func (ak AccountKeeper) GetModuleAccount(ctx sdk.Context, moduleName string) typ } // SetModuleAccount sets the module account to the auth account store -func (ak AccountKeeper) SetModuleAccount(ctx sdk.Context, macc types.ModuleAccountI) { //nolint:interfacer +func (ak AccountKeeper) SetModuleAccount(ctx sdk.Context, macc types.ModuleAccountI) { ak.SetAccount(ctx, macc) } diff --git a/x/auth/keeper/migrations.go b/x/auth/keeper/migrations.go new file mode 100644 index 000000000000..d3ad7a2f8c5a --- /dev/null +++ b/x/auth/keeper/migrations.go @@ -0,0 +1,43 @@ +package keeper + +import ( + "github.com/gogo/protobuf/grpc" + + v043 "github.com/cosmos/cosmos-sdk/x/auth/legacy/v043" + "github.com/cosmos/cosmos-sdk/x/auth/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Migrator is a struct for handling in-place store migrations. +type Migrator struct { + keeper AccountKeeper + queryServer grpc.Server +} + +// NewMigrator returns a new Migrator. +func NewMigrator(keeper AccountKeeper, queryServer grpc.Server) Migrator { + return Migrator{keeper: keeper, queryServer: queryServer} +} + +// Migrate1to2 migrates from version 1 to 2. +func (m Migrator) Migrate1to2(ctx sdk.Context) error { + var iterErr error + + m.keeper.IterateAccounts(ctx, func(account types.AccountI) (stop bool) { + wb, err := v043.MigrateAccount(ctx, account, m.queryServer) + if err != nil { + iterErr = err + return true + } + + if wb == nil { + return false + } + + m.keeper.SetAccount(ctx, wb) + return false + }) + + return iterErr +} diff --git a/x/auth/legacy/v043/store.go b/x/auth/legacy/v043/store.go new file mode 100644 index 000000000000..27db787f846b --- /dev/null +++ b/x/auth/legacy/v043/store.go @@ -0,0 +1,269 @@ +package v043 + +import ( + "errors" + "fmt" + + "github.com/gogo/protobuf/grpc" + "github.com/gogo/protobuf/proto" + abci "github.com/tendermint/tendermint/abci/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +const ( + delegatorDelegationPath = "/cosmos.staking.v1beta1.Query/DelegatorDelegations" + stakingParamsPath = "/cosmos.staking.v1beta1.Query/Params" + delegatorUnbondingDelegationsPath = "/cosmos.staking.v1beta1.Query/DelegatorUnbondingDelegations" + balancesPath = "/cosmos.bank.v1beta1.Query/AllBalances" +) + +func migrateVestingAccounts(ctx sdk.Context, account types.AccountI, queryServer grpc.Server) (types.AccountI, error) { + bondDenom, err := getBondDenom(ctx, queryServer) + + if err != nil { + return nil, err + } + + asVesting, ok := account.(exported.VestingAccount) + if !ok { + return nil, nil + } + + addr := account.GetAddress().String() + balance, err := getBalance( + ctx, + addr, + queryServer, + ) + + if err != nil { + return nil, err + } + + delegations, err := getDelegatorDelegationsSum( + ctx, + addr, + queryServer, + ) + + if err != nil { + return nil, err + } + + unbondingDelegations, err := getDelegatorUnbondingDelegationsSum( + ctx, + addr, + bondDenom, + queryServer, + ) + + if err != nil { + return nil, err + } + + delegations = delegations.Add(unbondingDelegations...) + + asVesting, ok = resetVestingDelegatedBalances(asVesting) + if !ok { + return nil, nil + } + + // balance before any delegation includes balance of delegation + for _, coin := range delegations { + balance = balance.Add(coin) + } + + asVesting.TrackDelegation(ctx.BlockTime(), balance, delegations) + + return asVesting.(types.AccountI), nil +} + +func resetVestingDelegatedBalances(evacct exported.VestingAccount) (exported.VestingAccount, bool) { + // reset `DelegatedVesting` and `DelegatedFree` to zero + df := sdk.NewCoins() + dv := sdk.NewCoins() + + switch vacct := evacct.(type) { + case *vestingtypes.ContinuousVestingAccount: + vacct.DelegatedVesting = dv + vacct.DelegatedFree = df + return vacct, true + case *vestingtypes.DelayedVestingAccount: + vacct.DelegatedVesting = dv + vacct.DelegatedFree = df + return vacct, true + case *vestingtypes.PeriodicVestingAccount: + vacct.DelegatedVesting = dv + vacct.DelegatedFree = df + return vacct, true + default: + return nil, false + } +} + +func getDelegatorDelegationsSum(ctx sdk.Context, address string, queryServer grpc.Server) (sdk.Coins, error) { + querier, ok := queryServer.(*baseapp.GRPCQueryRouter) + if !ok { + return nil, fmt.Errorf("unexpected type: %T wanted *baseapp.GRPCQueryRouter", queryServer) + } + + queryFn := querier.Route(delegatorDelegationPath) + + q := &stakingtypes.QueryDelegatorDelegationsRequest{ + DelegatorAddr: address, + } + + b, err := proto.Marshal(q) + if err != nil { + return nil, fmt.Errorf("cannot marshal staking type query request, %w", err) + } + req := abci.RequestQuery{ + Data: b, + Path: delegatorDelegationPath, + } + resp, err := queryFn(ctx, req) + if err != nil { + e, ok := status.FromError(err) + if ok && e.Code() == codes.NotFound { + return nil, nil + } + return nil, fmt.Errorf("staking query error, %w", err) + } + + balance := new(stakingtypes.QueryDelegatorDelegationsResponse) + if err := proto.Unmarshal(resp.Value, balance); err != nil { + return nil, fmt.Errorf("unable to unmarshal delegator query delegations: %w", err) + } + + res := sdk.NewCoins() + for _, i := range balance.DelegationResponses { + res = res.Add(i.Balance) + } + + return res, nil +} + +func getDelegatorUnbondingDelegationsSum(ctx sdk.Context, address, bondDenom string, queryServer grpc.Server) (sdk.Coins, error) { + querier, ok := queryServer.(*baseapp.GRPCQueryRouter) + if !ok { + return nil, fmt.Errorf("unexpected type: %T wanted *baseapp.GRPCQueryRouter", queryServer) + } + + queryFn := querier.Route(delegatorUnbondingDelegationsPath) + + q := &stakingtypes.QueryDelegatorUnbondingDelegationsRequest{ + DelegatorAddr: address, + } + + b, err := proto.Marshal(q) + if err != nil { + return nil, fmt.Errorf("cannot marshal staking type query request, %w", err) + } + req := abci.RequestQuery{ + Data: b, + Path: delegatorUnbondingDelegationsPath, + } + resp, err := queryFn(ctx, req) + if err != nil && !errors.Is(err, sdkerrors.ErrNotFound) { + e, ok := status.FromError(err) + if ok && e.Code() == codes.NotFound { + return nil, nil + } + return nil, fmt.Errorf("staking query error, %w", err) + } + + balance := new(stakingtypes.QueryDelegatorUnbondingDelegationsResponse) + if err := proto.Unmarshal(resp.Value, balance); err != nil { + return nil, fmt.Errorf("unable to unmarshal delegator query delegations: %w", err) + } + + res := sdk.NewCoins() + for _, i := range balance.UnbondingResponses { + for _, r := range i.Entries { + res = res.Add(sdk.NewCoin(bondDenom, r.Balance)) + } + } + + return res, nil +} + +func getBalance(ctx sdk.Context, address string, queryServer grpc.Server) (sdk.Coins, error) { + querier, ok := queryServer.(*baseapp.GRPCQueryRouter) + if !ok { + return nil, fmt.Errorf("unexpected type: %T wanted *baseapp.GRPCQueryRouter", queryServer) + } + + queryFn := querier.Route(balancesPath) + + q := &banktypes.QueryAllBalancesRequest{ + Address: address, + Pagination: nil, + } + b, err := proto.Marshal(q) + if err != nil { + return nil, fmt.Errorf("cannot marshal bank type query request, %w", err) + } + + req := abci.RequestQuery{ + Data: b, + Path: balancesPath, + } + resp, err := queryFn(ctx, req) + if err != nil { + return nil, fmt.Errorf("bank query error, %w", err) + } + balance := new(banktypes.QueryAllBalancesResponse) + if err := proto.Unmarshal(resp.Value, balance); err != nil { + return nil, fmt.Errorf("unable to unmarshal bank balance response: %w", err) + } + return balance.Balances, nil +} + +func getBondDenom(ctx sdk.Context, queryServer grpc.Server) (string, error) { + querier, ok := queryServer.(*baseapp.GRPCQueryRouter) + if !ok { + return "", fmt.Errorf("unexpected type: %T wanted *baseapp.GRPCQueryRouter", queryServer) + } + + queryFn := querier.Route(stakingParamsPath) + + q := &stakingtypes.QueryParamsRequest{} + + b, err := proto.Marshal(q) + if err != nil { + return "", fmt.Errorf("cannot marshal staking params query request, %w", err) + } + req := abci.RequestQuery{ + Data: b, + Path: stakingParamsPath, + } + + resp, err := queryFn(ctx, req) + if err != nil { + return "", fmt.Errorf("staking query error, %w", err) + } + + params := new(stakingtypes.QueryParamsResponse) + if err := proto.Unmarshal(resp.Value, params); err != nil { + return "", fmt.Errorf("unable to unmarshal delegator query delegations: %w", err) + } + + return params.Params.BondDenom, nil +} + +// MigrateAccount migrates vesting account to make the DelegatedVesting and DelegatedFree fields correctly +// track delegations. +// References: https://github.com/cosmos/cosmos-sdk/issues/8601, https://github.com/cosmos/cosmos-sdk/issues/8812 +func MigrateAccount(ctx sdk.Context, account types.AccountI, queryServer grpc.Server) (types.AccountI, error) { + return migrateVestingAccounts(ctx, account, queryServer) +} diff --git a/x/auth/legacy/v043/store_test.go b/x/auth/legacy/v043/store_test.go new file mode 100644 index 000000000000..acd06fe8783c --- /dev/null +++ b/x/auth/legacy/v043/store_test.go @@ -0,0 +1,687 @@ +package v043_test + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/cosmos/cosmos-sdk/simapp" + sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func TestMigrateVestingAccounts(t *testing.T) { + testCases := []struct { + name string + prepareFunc func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) + garbageFunc func(ctx sdk.Context, vesting exported.VestingAccount, app *simapp.SimApp) error + tokenAmount int64 + expVested int64 + expFree int64 + blockTime int64 + }{ + { + "delayed vesting has vested, multiple delegations less than the total account balance", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(200))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().Unix()) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 0, + 300, + 0, + }, + { + "delayed vesting has vested, single delegations which exceed the vested amount", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(200))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().Unix()) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 0, + 300, + 0, + }, + { + "delayed vesting has vested, multiple delegations which exceed the vested amount", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(200))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().Unix()) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 0, + 300, + 0, + }, + { + "delayed vesting has not vested, single delegations which exceed the vested amount", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(200))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(1, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 200, + 100, + 0, + }, + { + "delayed vesting has not vested, multiple delegations which exceed the vested amount", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(200))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(1, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 200, + 100, + 0, + }, + { + "not end time", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(1, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + _, err = app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(100), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 300, + 0, + 0, + }, + { + "delayed vesting has not vested, single delegation greater than the total account balance", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(1, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 300, + 0, + 0, + }, + { + "delayed vesting has vested, single delegation greater than the total account balance", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().Unix()) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 0, + 300, + 0, + }, + { + "continuous vesting, start time after blocktime", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + startTime := ctx.BlockTime().AddDate(1, 0, 0).Unix() + endTime := ctx.BlockTime().AddDate(2, 0, 0).Unix() + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + delayedAccount := types.NewContinuousVestingAccount(baseAccount, vestedCoins, startTime, endTime) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 300, + 0, + 0, + }, + { + "continuous vesting, start time passed but not ended", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + startTime := ctx.BlockTime().AddDate(-1, 0, 0).Unix() + endTime := ctx.BlockTime().AddDate(2, 0, 0).Unix() + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + delayedAccount := types.NewContinuousVestingAccount(baseAccount, vestedCoins, startTime, endTime) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 200, + 100, + 0, + }, + { + "continuous vesting, start time and endtime passed", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + startTime := ctx.BlockTime().AddDate(-2, 0, 0).Unix() + endTime := ctx.BlockTime().AddDate(-1, 0, 0).Unix() + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + delayedAccount := types.NewContinuousVestingAccount(baseAccount, vestedCoins, startTime, endTime) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 0, + 300, + 0, + }, + { + "periodic vesting account, yet to be vested, some rewards delegated", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(100))) + + start := ctx.BlockTime().Unix() + int64(time.Hour/time.Second) + + periods := []types.Period{ + { + Length: int64((24 * time.Hour) / time.Second), + Amount: vestedCoins, + }, + } + + account := types.NewPeriodicVestingAccount(baseAccount, vestedCoins, start, periods) + + app.AccountKeeper.SetAccount(ctx, account) + + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(150), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 300, + 100, + 50, + 0, + }, + { + "periodic vesting account, nothing has vested yet", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + /* + Test case: + - periodic vesting account starts at time 1601042400 + - account balance and original vesting: 3666666670000 + - nothing has vested, we put the block time slightly after start time + - expected vested: original vesting amount + - expected free: zero + - we're delegating the full original vesting + */ + startTime := int64(1601042400) + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(3666666670000))) + periods := []types.Period{ + { + Length: 31536000, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(1833333335000))), + }, + { + Length: 15638400, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + { + Length: 15897600, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + } + + delayedAccount := types.NewPeriodicVestingAccount(baseAccount, vestedCoins, startTime, periods) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + // delegation of the original vesting + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(3666666670000), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 3666666670000, + 3666666670000, + 0, + 1601042400 + 1, + }, + { + "periodic vesting account, all has vested", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + /* + Test case: + - periodic vesting account starts at time 1601042400 + - account balance and original vesting: 3666666670000 + - all has vested, so we set the block time at initial time + sum of all periods times + 1 => 1601042400 + 31536000 + 15897600 + 15897600 + 1 + - expected vested: zero + - expected free: original vesting amount + - we're delegating the full original vesting + */ + startTime := int64(1601042400) + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(3666666670000))) + periods := []types.Period{ + { + Length: 31536000, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(1833333335000))), + }, + { + Length: 15638400, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + { + Length: 15897600, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + } + + delayedAccount := types.NewPeriodicVestingAccount(baseAccount, vestedCoins, startTime, periods) + + ctx = ctx.WithBlockTime(time.Unix(1601042400+31536000+15897600+15897600+1, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + // delegation of the original vesting + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(3666666670000), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 3666666670000, + 0, + 3666666670000, + 1601042400 + 31536000 + 15897600 + 15897600 + 1, + }, + { + "periodic vesting account, first period has vested", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + /* + Test case: + - periodic vesting account starts at time 1601042400 + - account balance and original vesting: 3666666670000 + - first period have vested, so we set the block time at initial time + time of the first periods + 1 => 1601042400 + 31536000 + 1 + - expected vested: original vesting - first period amount + - expected free: first period amount + - we're delegating the full original vesting + */ + startTime := int64(1601042400) + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(3666666670000))) + periods := []types.Period{ + { + Length: 31536000, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(1833333335000))), + }, + { + Length: 15638400, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + { + Length: 15897600, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + } + + delayedAccount := types.NewPeriodicVestingAccount(baseAccount, vestedCoins, startTime, periods) + + ctx = ctx.WithBlockTime(time.Unix(1601042400+31536000+1, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + // delegation of the original vesting + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(3666666670000), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 3666666670000, + 3666666670000 - 1833333335000, + 1833333335000, + 1601042400 + 31536000 + 1, + }, + { + "periodic vesting account, first 2 period has vested", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + /* + Test case: + - periodic vesting account starts at time 1601042400 + - account balance and original vesting: 3666666670000 + - first 2 periods have vested, so we set the block time at initial time + time of the two periods + 1 => 1601042400 + 31536000 + 15638400 + 1 + - expected vested: original vesting - (sum of the first two periods amounts) + - expected free: sum of the first two periods + - we're delegating the full original vesting + */ + startTime := int64(1601042400) + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(3666666670000))) + periods := []types.Period{ + { + Length: 31536000, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(1833333335000))), + }, + { + Length: 15638400, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + { + Length: 15897600, + Amount: sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(916666667500))), + }, + } + + delayedAccount := types.NewPeriodicVestingAccount(baseAccount, vestedCoins, startTime, periods) + + ctx = ctx.WithBlockTime(time.Unix(1601042400+31536000+15638400+1, 0)) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + // delegation of the original vesting + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(3666666670000), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + }, + cleartTrackingFields, + 3666666670000, + 3666666670000 - 1833333335000 - 916666667500, + 1833333335000 + 916666667500, + 1601042400 + 31536000 + 15638400 + 1, + }, + { + "vesting account has unbonding delegations in place", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(10, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + + // delegation of the original vesting + _, err := app.StakingKeeper.Delegate(ctx, delegatorAddr, sdk.NewInt(300), stakingtypes.Unbonded, validator, true) + require.NoError(t, err) + + ctx = ctx.WithBlockTime(ctx.BlockTime().AddDate(1, 0, 0)) + + valAddr, err := sdk.ValAddressFromBech32(validator.OperatorAddress) + require.NoError(t, err) + + // un-delegation of the original vesting + _, err = app.StakingKeeper.Undelegate(ctx, delegatorAddr, valAddr, sdk.NewDecFromInt(sdk.NewInt(300))) + require.NoError(t, err) + }, + cleartTrackingFields, + 450, + 300, + 0, + 0, + }, + { + "vesting account has never delegated anything", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(10, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + }, + cleartTrackingFields, + 450, + 0, + 0, + 0, + }, + { + "vesting account has no delegation but dirty DelegatedFree and DelegatedVesting fields", + func(app *simapp.SimApp, ctx sdk.Context, validator stakingtypes.Validator, delegatorAddr sdk.AccAddress) { + baseAccount := authtypes.NewBaseAccountWithAddress(delegatorAddr) + vestedCoins := sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(300))) + + delayedAccount := types.NewDelayedVestingAccount(baseAccount, vestedCoins, ctx.BlockTime().AddDate(10, 0, 0).Unix()) + + app.AccountKeeper.SetAccount(ctx, delayedAccount) + }, + dirtyTrackingFields, + 450, + 0, + 0, + 0, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{ + Time: time.Now(), + }) + + addrs := simapp.AddTestAddrs(app, ctx, 1, sdk.NewInt(tc.tokenAmount)) + delegatorAddr := addrs[0] + + _, valAddr := createValidator(t, ctx, app, tc.tokenAmount*2) + validator, found := app.StakingKeeper.GetValidator(ctx, valAddr) + require.True(t, found) + + tc.prepareFunc(app, ctx, validator, delegatorAddr) + + if tc.blockTime != 0 { + ctx = ctx.WithBlockTime(time.Unix(tc.blockTime, 0)) + } + + // We introduce the bug + savedAccount := app.AccountKeeper.GetAccount(ctx, delegatorAddr) + vestingAccount, ok := savedAccount.(exported.VestingAccount) + require.True(t, ok) + require.NoError(t, tc.garbageFunc(ctx, vestingAccount, app)) + + m := authkeeper.NewMigrator(app.AccountKeeper, app.GRPCQueryRouter()) + require.NoError(t, m.Migrate1to2(ctx)) + + var expVested sdk.Coins + var expFree sdk.Coins + + if tc.expVested != 0 { + expVested = sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(tc.expVested))) + } + + if tc.expFree != 0 { + expFree = sdk.NewCoins(sdk.NewCoin(app.StakingKeeper.BondDenom(ctx), sdk.NewInt(tc.expFree))) + } + + trackingCorrected( + ctx, + t, + app.AccountKeeper, + savedAccount.GetAddress(), + expVested, + expFree, + ) + }) + } + +} + +func trackingCorrected(ctx sdk.Context, t *testing.T, ak authkeeper.AccountKeeper, addr sdk.AccAddress, expDelVesting sdk.Coins, expDelFree sdk.Coins) { + t.Helper() + baseAccount := ak.GetAccount(ctx, addr) + vDA, ok := baseAccount.(exported.VestingAccount) + require.True(t, ok) + + vestedOk := expDelVesting.IsEqual(vDA.GetDelegatedVesting()) + freeOk := expDelFree.IsEqual(vDA.GetDelegatedFree()) + require.True(t, vestedOk, vDA.GetDelegatedVesting().String()) + require.True(t, freeOk, vDA.GetDelegatedFree().String()) +} + +func cleartTrackingFields(ctx sdk.Context, vesting exported.VestingAccount, app *simapp.SimApp) error { + switch t := vesting.(type) { + case *types.DelayedVestingAccount: + t.DelegatedFree = nil + t.DelegatedVesting = nil + app.AccountKeeper.SetAccount(ctx, t) + case *types.ContinuousVestingAccount: + t.DelegatedFree = nil + t.DelegatedVesting = nil + app.AccountKeeper.SetAccount(ctx, t) + case *types.PeriodicVestingAccount: + t.DelegatedFree = nil + t.DelegatedVesting = nil + app.AccountKeeper.SetAccount(ctx, t) + default: + return fmt.Errorf("expected vesting account, found %t", t) + } + + return nil +} + +func dirtyTrackingFields(ctx sdk.Context, vesting exported.VestingAccount, app *simapp.SimApp) error { + dirt := sdk.NewCoins(sdk.NewInt64Coin("stake", 42)) + + switch t := vesting.(type) { + case *types.DelayedVestingAccount: + t.DelegatedFree = dirt + t.DelegatedVesting = dirt + app.AccountKeeper.SetAccount(ctx, t) + case *types.ContinuousVestingAccount: + t.DelegatedFree = dirt + t.DelegatedVesting = dirt + app.AccountKeeper.SetAccount(ctx, t) + case *types.PeriodicVestingAccount: + t.DelegatedFree = dirt + t.DelegatedVesting = dirt + app.AccountKeeper.SetAccount(ctx, t) + default: + return fmt.Errorf("expected vesting account, found %t", t) + } + + return nil +} + +func createValidator(t *testing.T, ctx sdk.Context, app *simapp.SimApp, powers int64) (sdk.AccAddress, sdk.ValAddress) { + valTokens := sdk.TokensFromConsensusPower(powers) + addrs := simapp.AddTestAddrsIncremental(app, ctx, 1, valTokens) + valAddrs := simapp.ConvertAddrsToValAddrs(addrs) + pks := simapp.CreateTestPubKeys(1) + cdc := simapp.MakeTestEncodingConfig().Marshaler + + app.StakingKeeper = stakingkeeper.NewKeeper( + cdc, + app.GetKey(stakingtypes.StoreKey), + app.AccountKeeper, + app.BankKeeper, + app.GetSubspace(stakingtypes.ModuleName), + ) + + val1, err := stakingtypes.NewValidator(valAddrs[0], pks[0], stakingtypes.Description{}) + require.NoError(t, err) + + app.StakingKeeper.SetValidator(ctx, val1) + require.NoError(t, app.StakingKeeper.SetValidatorByConsAddr(ctx, val1)) + app.StakingKeeper.SetNewValidatorByPowerIndex(ctx, val1) + + _, err = app.StakingKeeper.Delegate(ctx, addrs[0], valTokens, stakingtypes.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, app.StakingKeeper) + + return addrs[0], valAddrs[0] +} diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index 10ab72790c5c..1c239bf434da 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -1,8 +1,6 @@ package keeper import ( - "time" - "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" @@ -108,7 +106,7 @@ func (k BaseKeeper) DelegateCoins(ctx sdk.Context, delegatorAddr, moduleAccAddr } } - if err := k.trackDelegation(ctx, delegatorAddr, ctx.BlockHeader().Time, balances, amt); err != nil { + if err := k.trackDelegation(ctx, delegatorAddr, balances, amt); err != nil { return sdkerrors.Wrap(err, "failed to track delegation") } @@ -382,7 +380,7 @@ func (k BaseKeeper) BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) return nil } -func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, blockTime time.Time, balance, amt sdk.Coins) error { +func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, balance, amt sdk.Coins) error { acc := k.ak.GetAccount(ctx, addr) if acc == nil { return sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "account %s does not exist", addr) @@ -391,7 +389,8 @@ func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, blockT vacc, ok := acc.(vestexported.VestingAccount) if ok { // TODO: return error on account.TrackDelegation - vacc.TrackDelegation(blockTime, balance, amt) + vacc.TrackDelegation(ctx.BlockHeader().Time, balance, amt) + k.ak.SetAccount(ctx, acc) } return nil @@ -407,6 +406,7 @@ func (k BaseKeeper) trackUndelegation(ctx sdk.Context, addr sdk.AccAddress, amt if ok { // TODO: return error on account.TrackUndelegation vacc.TrackUndelegation(amt) + k.ak.SetAccount(ctx, acc) } return nil diff --git a/x/bank/keeper/keeper_test.go b/x/bank/keeper/keeper_test.go index fee8f2a77c85..35083c5a16af 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -9,6 +9,8 @@ import ( tmproto "github.com/tendermint/tendermint/proto/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" + "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" @@ -905,6 +907,12 @@ func (suite *IntegrationTestSuite) TestDelegateCoins() { // require the ability for a vesting account to delegate suite.Require().NoError(app.BankKeeper.DelegateCoins(ctx, addr1, addrModule, delCoins)) suite.Require().Equal(delCoins, app.BankKeeper.GetAllBalances(ctx, addr1)) + + // require that delegated vesting amount is equal to what was delegated with DelegateCoins + acc = app.AccountKeeper.GetAccount(ctx, addr1) + vestingAcc, ok := acc.(exported.VestingAccount) + suite.Require().True(ok) + suite.Require().Equal(delCoins, vestingAcc.GetDelegatedVesting()) } func (suite *IntegrationTestSuite) TestDelegateCoins_Invalid() { @@ -982,6 +990,12 @@ func (suite *IntegrationTestSuite) TestUndelegateCoins() { suite.Require().Equal(origCoins, app.BankKeeper.GetAllBalances(ctx, addr1)) suite.Require().True(app.BankKeeper.GetAllBalances(ctx, addrModule).Empty()) + + // require that delegated vesting amount is completely empty, since they were completely undelegated + acc = app.AccountKeeper.GetAccount(ctx, addr1) + vestingAcc, ok := acc.(exported.VestingAccount) + suite.Require().True(ok) + suite.Require().Empty(vestingAcc.GetDelegatedVesting()) } func (suite *IntegrationTestSuite) TestUndelegateCoins_Invalid() {