diff --git a/x/auth/keeper/keeper.go b/x/auth/keeper/keeper.go index 3ead1674e742..62a10e26ba52 100644 --- a/x/auth/keeper/keeper.go +++ b/x/auth/keeper/keeper.go @@ -206,7 +206,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..20f60f6abb32 --- /dev/null +++ b/x/auth/keeper/migrations.go @@ -0,0 +1,42 @@ +package keeper + +import ( + v043 "github.com/cosmos/cosmos-sdk/x/auth/legacy/v043" + "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/gogo/protobuf/grpc" + + 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..7abb2917cab5 --- /dev/null +++ b/x/auth/legacy/v043/store.go @@ -0,0 +1,268 @@ +package v043 + +import ( + "errors" + "fmt" + + "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" + "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" +) + +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/auth/module.go b/x/auth/module.go index ff053d8c7c76..f0c26cd97e59 100644 --- a/x/auth/module.go +++ b/x/auth/module.go @@ -129,6 +129,11 @@ func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sd // module-specific GRPC queries. func (am AppModule) RegisterServices(cfg module.Configurator) { types.RegisterQueryServer(cfg.QueryServer(), am.accountKeeper) + m := keeper.NewMigrator(am.accountKeeper, cfg.QueryServer()) + err := cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2) + if err != nil { + panic(err) + } } // InitGenesis performs genesis initialization for the auth module. It returns @@ -148,7 +153,7 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONMarshaler) json } // ConsensusVersion implements AppModule/ConsensusVersion. -func (AppModule) ConsensusVersion() uint64 { return 1 } +func (AppModule) ConsensusVersion() uint64 { return 2 } // BeginBlock returns the begin blocker for the auth module. func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} diff --git a/x/bank/keeper/keeper.go b/x/bank/keeper/keeper.go index 34eb434c6fba..a5b05b044ab8 100644 --- a/x/bank/keeper/keeper.go +++ b/x/bank/keeper/keeper.go @@ -446,6 +446,7 @@ func (k BaseKeeper) trackDelegation(ctx sdk.Context, addr sdk.AccAddress, balanc if ok { // TODO: return error on account.TrackDelegation vacc.TrackDelegation(ctx.BlockHeader().Time, balance, amt) + k.ak.SetAccount(ctx, acc) } return nil @@ -461,6 +462,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 52d32db4fc7f..2c2516e16510 100644 --- a/x/bank/keeper/keeper_test.go +++ b/x/bank/keeper/keeper_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" "github.com/stretchr/testify/suite" @@ -904,6 +905,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() { @@ -978,6 +985,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() {