From cc23c629b620865c998becd553151d277cb2c655 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Tue, 20 Sep 2022 06:23:21 -0400 Subject: [PATCH] fix: ensure withdraw_rewards events are always emitted on reward withdrawal (#13323) (cherry picked from commit c1c23a75d932a31acc907d896f0fb66194bdbc18) # Conflicts: # CHANGELOG.md # tests/integration/distribution/keeper/delegation_test.go # testutil/sims/app_helpers.go # x/distribution/keeper/delegation.go # x/distribution/keeper/delegation_test.go # x/distribution/keeper/keeper.go --- CHANGELOG.md | 35 + .../distribution/keeper/delegation_test.go | 820 ++++++++++++++++++ testutil/sims/app_helpers.go | 287 ++++++ x/distribution/keeper/delegation.go | 34 +- x/distribution/keeper/delegation_test.go | 16 +- x/distribution/keeper/keeper.go | 3 + 6 files changed, 1188 insertions(+), 7 deletions(-) create mode 100644 tests/integration/distribution/keeper/delegation_test.go create mode 100644 testutil/sims/app_helpers.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3e525f3cab..36d572ada8f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,41 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Improvements +<<<<<<< HEAD +======= +* [#13323](https://github.com/cosmos/cosmos-sdk/pull/13323) Ensure `withdraw_rewards` rewards are emitted from all actions that result in rewards being withdrawn. +* [#13214](https://github.com/cosmos/cosmos-sdk/pull/13214) Add `withdraw-proposal` command to group module's CLI transaction commands. +* [#13070](https://github.com/cosmos/cosmos-sdk/pull/13070) Migrate from `gogo/protobuf` to `cosmos/gogoproto`. +* [#12981](https://github.com/cosmos/cosmos-sdk/pull/12981) Return proper error when parsing telemetry configuration. +* [#12995](https://github.com/cosmos/cosmos-sdk/pull/12995) Add `FormatTime` and `ParseTimeString` methods. +* [#12952](https://github.com/cosmos/cosmos-sdk/pull/12952) Replace keyring module to Cosmos fork. +* [#12352](https://github.com/cosmos/cosmos-sdk/pull/12352) Move the `RegisterSwaggerAPI` logic into a separate helper function in the server package. +* [#12876](https://github.com/cosmos/cosmos-sdk/pull/12876) Remove proposer-based rewards. +* [#12892](https://github.com/cosmos/cosmos-sdk/pull/12892) `make format` now runs only gofumpt and golangci-lint run ./... --fix, replacing `goimports` `gofmt` and `misspell` +* [#12846](https://github.com/cosmos/cosmos-sdk/pull/12846) Remove `RandomizedParams` from the `AppModuleSimulation` interface which is no longer needed. +* (events) [#12850](https://github.com/cosmos/cosmos-sdk/pull/12850) Add a new `fee_payer` attribute to the `tx` event that is emitted from the `DeductFeeDecorator` AnteHandler decorator. +* (ci) [#12854](https://github.com/cosmos/cosmos-sdk/pull/12854) Use ghcr.io to host the proto builder image. Update proto builder image to go 1.19 +* (x/bank) [#12706](https://github.com/cosmos/cosmos-sdk/pull/12706) Added the `chain-id` flag to the `AddTxFlagsToCmd` API. There is no longer a need to explicitly register this flag on commands whens `AddTxFlagsToCmd` is already called. +* [#12791](https://github.com/cosmos/cosmos-sdk/pull/12791) Bump the math library used in the sdk and replace old usages of sdk.\* +* (x/params) [#12615](https://github.com/cosmos/cosmos-sdk/pull/12615) Add `GetParamSetIfExists` function to params `Subspace` to prevent panics on breaking changes. +* [#12717](https://github.com/cosmos/cosmos-sdk/pull/12717) Use injected encoding params in simapp. +* (x/bank) [#12674](https://github.com/cosmos/cosmos-sdk/pull/12674) Add convenience function `CreatePrefixedAccountStoreKey()` to construct key to access account's balance for a given denom. +* [#12702](https://github.com/cosmos/cosmos-sdk/pull/12702) Linting and tidiness, fixed two minor security warnings. +* [#12634](https://github.com/cosmos/cosmos-sdk/pull/12634) Move `sdk.Dec` to math package. +* [#12596](https://github.com/cosmos/cosmos-sdk/pull/12596) Remove all imports of the non-existent gogo/protobuf v1.3.3 to ease downstream use and go workspaces. +* [#12187](https://github.com/cosmos/cosmos-sdk/pull/12187) Add batch operation for x/nft module. +* [#12693](https://github.com/cosmos/cosmos-sdk/pull/12693) Make sure the order of each node is consistent when emitting proto events. +* [#12455](https://github.com/cosmos/cosmos-sdk/pull/12455) Show attempts count in error for signing. +* [#12886](https://github.com/cosmos/cosmos-sdk/pull/12886) Amortize cost of processing cache KV store +* [#12953](https://github.com/cosmos/cosmos-sdk/pull/12953) Change the default priority mechanism to be based on gas price. +* [#13048](https://github.com/cosmos/cosmos-sdk/pull/13048) Add handling of AccountNumberStoreKeyPrefix to the x/auth simulation decoder. +* [#13101](https://github.com/cosmos/cosmos-sdk/pull/13101) Remove weights from `simapp/params` and `testutil/sims`. They are now in their respective modules. +* (simapp) [#13107](https://github.com/cosmos/cosmos-sdk/pull/13107) Call `SetIAVLCacheSize` with the configured value in simapp. +* [#12398](https://github.com/cosmos/cosmos-sdk/issues/12398) Refactor all `x` modules to unit-test via mocks and decouple `simapp`. +* [#13144](https://github.com/cosmos/cosmos-sdk/pull/13144) Add validator distribution info grpc gateway get endpoint. +* [#13168](https://github.com/cosmos/cosmos-sdk/pull/13168) Migrate tendermintdev/proto-builder to ghcr.io. New image `ghcr.io/cosmos/proto-builder:0.8` +* [#13178](https://github.com/cosmos/cosmos-sdk/pull/13178) Add `cosmos.msg.v1.service` protobuf annotation to allow tooling to distinguish between Msg and Query services via reflection. +>>>>>>> c1c23a75d (fix: ensure withdraw_rewards events are always emitted on reward withdrawal (#13323)) * [#13233](https://github.com/cosmos/cosmos-sdk/pull/13233) Add `--append` to `add-genesis-account` sub-command to append new tokens after an account is already created. * (x/group) [#13214](https://github.com/cosmos/cosmos-sdk/pull/13214) Add `withdraw-proposal` command to group module's CLI transaction commands. * (x/auth) [#13048](https://github.com/cosmos/cosmos-sdk/pull/13048) Add handling of AccountNumberStoreKeyPrefix to the simulation decoder. diff --git a/tests/integration/distribution/keeper/delegation_test.go b/tests/integration/distribution/keeper/delegation_test.go new file mode 100644 index 000000000000..0bb991158114 --- /dev/null +++ b/tests/integration/distribution/keeper/delegation_test.go @@ -0,0 +1,820 @@ +package keeper_test + +import ( + "testing" + + "cosmossdk.io/math" + "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" + bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" + banktestutil "github.com/cosmos/cosmos-sdk/x/bank/testutil" + "github.com/cosmos/cosmos-sdk/x/distribution/keeper" + "github.com/cosmos/cosmos-sdk/x/distribution/testutil" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + "github.com/cosmos/cosmos-sdk/x/staking/teststaking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func TestCalculateRewardsBasic(t *testing.T) { + var ( + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + distrKeeper.DeleteAllValidatorHistoricalRewards(ctx) + + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(1000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + + // create validator with 50% commission + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + tstaking.CreateValidator(valAddrs[0], valConsPk0, sdk.NewInt(100), true) + + // end block to bond validator and start new block + staking.EndBlocker(ctx, stakingKeeper) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + tstaking.Ctx = ctx + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // historical count should be 2 (once for validator init, once for delegation init) + require.Equal(t, uint64(2), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // historical count should be 2 still + require.Equal(t, uint64(2), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // calculate delegation rewards + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // allocate some rewards + initial := int64(10) + tokens := sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial)}} + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial / 2)}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial / 2)}}, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) +} + +func TestCalculateRewardsAfterSlash(t *testing.T) { + var ( + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(100000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + + // create validator with 50% commission + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + valPower := int64(100) + tstaking.CreateValidatorWithValPower(valAddrs[0], valConsPk0, valPower, true) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // start out block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // slash the validator by 50% + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower, sdk.NewDecWithPrec(5, 1)) + + // retrieve validator + val = stakingKeeper.Validator(ctx, valAddrs[0]) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some rewards + initial := stakingKeeper.TokensFromConsensusPower(ctx, 10) + tokens := sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: sdk.NewDecFromInt(initial)}} + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: sdk.NewDecFromInt(initial.QuoRaw(2))}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: sdk.NewDecFromInt(initial.QuoRaw(2))}}, + distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) +} + +func TestCalculateRewardsAfterManySlashes(t *testing.T) { + var ( + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(100000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + + // create validator with 50% commission + valPower := int64(100) + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + tstaking.CreateValidatorWithValPower(valAddrs[0], valConsPk0, valPower, true) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // start out block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // slash the validator by 50% + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower, sdk.NewDecWithPrec(5, 1)) + + // fetch the validator again + val = stakingKeeper.Validator(ctx, valAddrs[0]) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some rewards + initial := stakingKeeper.TokensFromConsensusPower(ctx, 10) + tokens := sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: sdk.NewDecFromInt(initial)}} + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator by 50% again + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower/2, sdk.NewDecWithPrec(5, 1)) + + // fetch the validator again + val = stakingKeeper.Validator(ctx, valAddrs[0]) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: sdk.NewDecFromInt(initial)}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: sdk.NewDecFromInt(initial)}}, + distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) +} + +func TestCalculateRewardsMultiDelegator(t *testing.T) { + var ( + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(100000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + + // create validator with 50% commission + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + tstaking.CreateValidator(valAddrs[0], valConsPk0, sdk.NewInt(100), true) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del1 := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // allocate some rewards + initial := int64(20) + tokens := sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial)}} + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // second delegation + tstaking.Ctx = ctx + tstaking.Delegate(sdk.AccAddress(valAddrs[1]), valAddrs[0], sdk.NewInt(100)) + del2 := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[1]), valAddrs[0]) + + // fetch updated validator + val = stakingKeeper.Validator(ctx, valAddrs[0]) + + // end block + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be 3/4 initial + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial * 3 / 4)}}, rewards) + + // calculate delegation rewards for del2 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be 1/4 initial + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial * 1 / 4)}}, rewards) + + // commission should be equal to initial (50% twice) + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial)}}, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) +} + +func TestWithdrawDelegationRewardsBasic(t *testing.T) { + var ( + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &accountKeeper, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + distrKeeper.DeleteAllValidatorHistoricalRewards(ctx) + + balancePower := int64(1000) + balanceTokens := stakingKeeper.TokensFromConsensusPower(ctx, balancePower) + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 1, sdk.NewInt(1000000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + + // set module account coins + distrAcc := distrKeeper.GetDistributionAccount(ctx) + require.NoError(t, banktestutil.FundModuleAccount(bankKeeper, ctx, distrAcc.GetName(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, balanceTokens)))) + accountKeeper.SetModuleAccount(ctx, distrAcc) + + // create validator with 50% commission + power := int64(100) + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + valTokens := tstaking.CreateValidatorWithValPower(valAddrs[0], valConsPk0, power, true) + + // assert correct initial balance + expTokens := balanceTokens.Sub(valTokens) + require.Equal(t, + sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, expTokens)}, + bankKeeper.GetAllBalances(ctx, sdk.AccAddress(valAddrs[0])), + ) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + + // allocate some rewards + initial := stakingKeeper.TokensFromConsensusPower(ctx, 10) + tokens := sdk.DecCoins{sdk.NewDecCoin(sdk.DefaultBondDenom, initial)} + + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // historical count should be 2 (initial + latest for delegation) + require.Equal(t, uint64(2), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // withdraw rewards + _, err = distrKeeper.WithdrawDelegationRewards(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + require.Nil(t, err) + + // historical count should still be 2 (added one record, cleared one) + require.Equal(t, uint64(2), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // assert correct balance + exp := balanceTokens.Sub(valTokens).Add(initial.QuoRaw(2)) + require.Equal(t, + sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, exp)}, + bankKeeper.GetAllBalances(ctx, sdk.AccAddress(valAddrs[0])), + ) + + // withdraw commission + _, err = distrKeeper.WithdrawValidatorCommission(ctx, valAddrs[0]) + require.Nil(t, err) + + // assert correct balance + exp = balanceTokens.Sub(valTokens).Add(initial) + require.Equal(t, + sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, exp)}, + bankKeeper.GetAllBalances(ctx, sdk.AccAddress(valAddrs[0])), + ) +} + +func TestCalculateRewardsAfterManySlashesInSameBlock(t *testing.T) { + var ( + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 1, sdk.NewInt(1000000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + + // create validator with 50% commission + valPower := int64(100) + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + tstaking.CreateValidatorWithValPower(valAddrs[0], valConsPk0, valPower, true) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // start out block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some rewards + initial := sdk.NewDecFromInt(stakingKeeper.TokensFromConsensusPower(ctx, 10)) + tokens := sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial}} + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator by 50% + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower, sdk.NewDecWithPrec(5, 1)) + + // slash the validator by 50% again + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower/2, sdk.NewDecWithPrec(5, 1)) + + // fetch the validator again + val = stakingKeeper.Validator(ctx, valAddrs[0]) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial}}, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) +} + +func TestCalculateRewardsMultiDelegatorMultiSlash(t *testing.T) { + var ( + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(1000000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + + // create validator with 50% commission + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + valPower := int64(100) + tstaking.CreateValidatorWithValPower(valAddrs[0], valConsPk0, valPower, true) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del1 := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // allocate some rewards + initial := sdk.NewDecFromInt(stakingKeeper.TokensFromConsensusPower(ctx, 30)) + tokens := sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial}} + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower, sdk.NewDecWithPrec(5, 1)) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // second delegation + tstaking.DelegateWithPower(sdk.AccAddress(valAddrs[1]), valAddrs[0], 100) + + del2 := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[1]), valAddrs[0]) + + // end block + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator again + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + stakingKeeper.Slash(ctx, valConsAddr0, ctx.BlockHeight(), valPower, sdk.NewDecWithPrec(5, 1)) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // fetch updated validator + val = stakingKeeper.Validator(ctx, valAddrs[0]) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be 2/3 initial (half initial first period, 1/6 initial second period) + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial.QuoInt64(2).Add(initial.QuoInt64(6))}}, rewards) + + // calculate delegation rewards for del2 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be initial / 3 + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial.QuoInt64(3)}}, rewards) + + // commission should be equal to initial (twice 50% commission, unaffected by slashing) + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: initial}}, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) +} + +func TestCalculateRewardsMultiDelegatorMultWithdraw(t *testing.T) { + var ( + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &accountKeeper, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + distrKeeper.DeleteAllValidatorHistoricalRewards(ctx) + + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(1000000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + initial := int64(20) + + // set module account coins + distrAcc := distrKeeper.GetDistributionAccount(ctx) + require.NoError(t, banktestutil.FundModuleAccount(bankKeeper, ctx, distrAcc.GetName(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000))))) + accountKeeper.SetModuleAccount(ctx, distrAcc) + + tokens := sdk.DecCoins{sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, math.LegacyNewDec(initial))} + + // create validator with 50% commission + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), math.LegacyNewDec(0)) + tstaking.CreateValidator(valAddrs[0], valConsPk0, sdk.NewInt(100), true) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator and delegation + val := stakingKeeper.Validator(ctx, valAddrs[0]) + del1 := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // allocate some rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // historical count should be 2 (validator init, delegation init) + require.Equal(t, uint64(2), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // second delegation + tstaking.Delegate(sdk.AccAddress(valAddrs[1]), valAddrs[0], sdk.NewInt(100)) + + // historical count should be 3 (second delegation init) + require.Equal(t, uint64(3), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // fetch updated validator + val = stakingKeeper.Validator(ctx, valAddrs[0]) + del2 := stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[1]), valAddrs[0]) + + // end block + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // first delegator withdraws + _, err = distrKeeper.WithdrawDelegationRewards(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + require.NoError(t, err) + + // second delegator withdraws + _, err = distrKeeper.WithdrawDelegationRewards(ctx, sdk.AccAddress(valAddrs[1]), valAddrs[0]) + require.NoError(t, err) + + // historical count should be 3 (validator init + two delegations) + require.Equal(t, uint64(3), distrKeeper.GetValidatorHistoricalReferenceCount(ctx)) + + // validator withdraws commission + _, err = distrKeeper.WithdrawValidatorCommission(ctx, valAddrs[0]) + require.NoError(t, err) + + // end period + endingPeriod := distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards := distrKeeper.CalculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be zero + require.True(t, rewards.IsZero()) + + // calculate delegation rewards for del2 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be zero + require.True(t, rewards.IsZero()) + + // commission should be zero + require.True(t, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission.IsZero()) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // first delegator withdraws again + _, err = distrKeeper.WithdrawDelegationRewards(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + require.NoError(t, err) + + // end period + endingPeriod = distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be zero + require.True(t, rewards.IsZero()) + + // calculate delegation rewards for del2 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be 1/4 initial + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial / 4)}}, rewards) + + // commission should be half initial + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial / 2)}}, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // withdraw commission + _, err = distrKeeper.WithdrawValidatorCommission(ctx, valAddrs[0]) + require.NoError(t, err) + + // end period + endingPeriod = distrKeeper.IncrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be 1/4 initial + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial / 4)}}, rewards) + + // calculate delegation rewards for del2 + rewards = distrKeeper.CalculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be 1/2 initial + require.Equal(t, sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: math.LegacyNewDec(initial / 2)}}, rewards) + + // commission should be zero + require.True(t, distrKeeper.GetValidatorAccumulatedCommission(ctx, valAddrs[0]).Commission.IsZero()) +} + +func Test100PercentCommissionReward(t *testing.T) { + var ( + accountKeeper authkeeper.AccountKeeper + bankKeeper bankkeeper.Keeper + distrKeeper keeper.Keeper + stakingKeeper *stakingkeeper.Keeper + ) + + app, err := simtestutil.Setup(testutil.AppConfig, + &accountKeeper, + &bankKeeper, + &distrKeeper, + &stakingKeeper, + ) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + tstaking := teststaking.NewHelper(t, ctx, stakingKeeper) + addr := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 2, sdk.NewInt(1000000000)) + valAddrs := simtestutil.ConvertAddrsToValAddrs(addr) + initial := int64(20) + + // set module account coins + distrAcc := distrKeeper.GetDistributionAccount(ctx) + require.NoError(t, banktestutil.FundModuleAccount(bankKeeper, ctx, distrAcc.GetName(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000))))) + accountKeeper.SetModuleAccount(ctx, distrAcc) + + tokens := sdk.DecCoins{sdk.NewDecCoinFromDec(sdk.DefaultBondDenom, math.LegacyNewDec(initial))} + + // create validator with 100% commission + tstaking.Commission = stakingtypes.NewCommissionRates(sdk.NewDecWithPrec(10, 1), sdk.NewDecWithPrec(10, 1), math.LegacyNewDec(0)) + tstaking.CreateValidator(valAddrs[0], valConsPk0, sdk.NewInt(100), true) + stakingKeeper.Delegation(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + + // end block to bond validator + staking.EndBlocker(ctx, stakingKeeper) + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // fetch validator + val := stakingKeeper.Validator(ctx, valAddrs[0]) + + // allocate some rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // end block + staking.EndBlocker(ctx, stakingKeeper) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + // next block + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + + // allocate some more rewards + distrKeeper.AllocateTokensToValidator(ctx, val, tokens) + + rewards, err := distrKeeper.WithdrawDelegationRewards(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) + require.NoError(t, err) + + zeroRewards := sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, math.ZeroInt())} + require.True(t, rewards.IsEqual(zeroRewards)) + + events := ctx.EventManager().Events() + lastEvent := events[len(events)-1] + + var hasValue bool + for _, attr := range lastEvent.Attributes { + if attr.Key == "amount" && attr.Value == "0stake" { + hasValue = true + } + } + require.True(t, hasValue) +} diff --git a/testutil/sims/app_helpers.go b/testutil/sims/app_helpers.go new file mode 100644 index 000000000000..08aa4597e15a --- /dev/null +++ b/testutil/sims/app_helpers.go @@ -0,0 +1,287 @@ +package sims + +import ( + "encoding/json" + "fmt" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + tmjson "github.com/tendermint/tendermint/libs/json" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtypes "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" + + "cosmossdk.io/depinject" + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/cosmos/cosmos-sdk/runtime" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/cosmos/cosmos-sdk/testutil/mock" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// SimAppChainID hardcoded chainID for simulation +const ( + DefaultGenTxGas = 10000000 + SimAppChainID = "simulation-app" +) + +// DefaultConsensusParams defines the default Tendermint consensus params used in +// SimApp testing. +var DefaultConsensusParams = &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxBytes: 200000, + MaxGas: 2000000, + }, + Evidence: &tmproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &tmproto.ValidatorParams{ + PubKeyTypes: []string{ + tmtypes.ABCIPubKeyTypeEd25519, + }, + }, +} + +// CreateRandomValidatorSet creates a validator set with one random validator +func CreateRandomValidatorSet() (*tmtypes.ValidatorSet, error) { + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + if err != nil { + return nil, fmt.Errorf("failed to get pub key: %w", err) + } + + // create validator set with single validator + validator := tmtypes.NewValidator(pubKey, 1) + + return tmtypes.NewValidatorSet([]*tmtypes.Validator{validator}), nil +} + +type GenesisAccount struct { + authtypes.GenesisAccount + Coins sdk.Coins +} + +// StartupConfig defines the startup configuration new a test application. +// +// ValidatorSet defines a custom validator set to be validating the app. +// BaseAppOption defines the additional operations that must be run on baseapp before app start. +// AtGenesis defines if the app started should already have produced block or not. +type StartupConfig struct { + ValidatorSet func() (*tmtypes.ValidatorSet, error) + BaseAppOption runtime.BaseAppOption + AtGenesis bool + GenesisAccounts []GenesisAccount +} + +func DefaultStartUpConfig() StartupConfig { + priv := secp256k1.GenPrivKey() + ba := authtypes.NewBaseAccount(priv.PubKey().Address().Bytes(), priv.PubKey(), 0, 0) + ga := GenesisAccount{ba, sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000)))} + return StartupConfig{ + ValidatorSet: CreateRandomValidatorSet, + AtGenesis: false, + GenesisAccounts: []GenesisAccount{ga}, + } +} + +// Setup initializes a new runtime.App and can inject values into extraOutputs. +// It uses SetupWithConfiguration under the hood. +func Setup(appConfig depinject.Config, extraOutputs ...interface{}) (*runtime.App, error) { + return SetupWithConfiguration(appConfig, DefaultStartUpConfig(), extraOutputs...) +} + +// SetupAtGenesis initializes a new runtime.App at genesis and can inject values into extraOutputs. +// It uses SetupWithConfiguration under the hood. +func SetupAtGenesis(appConfig depinject.Config, extraOutputs ...interface{}) (*runtime.App, error) { + cfg := DefaultStartUpConfig() + cfg.AtGenesis = true + return SetupWithConfiguration(appConfig, cfg, extraOutputs...) +} + +// SetupWithConfiguration initializes a new runtime.App. A Nop logger is set in runtime.App. +// appConfig usually load from a `app.yaml` with `appconfig.LoadYAML`, defines the application configuration. +// extraOutputs defines the extra outputs to be assigned by the dependency injector (depinject). +func SetupWithConfiguration(appConfig depinject.Config, startupConfig StartupConfig, extraOutputs ...interface{}) (*runtime.App, error) { + // create the app with depinject + var ( + app *runtime.App + appBuilder *runtime.AppBuilder + codec codec.Codec + ) + + if err := depinject.Inject( + appConfig, + append(extraOutputs, &appBuilder, &codec)..., + ); err != nil { + return nil, fmt.Errorf("failed to inject dependencies: %w", err) + } + + if startupConfig.BaseAppOption != nil { + app = appBuilder.Build(log.NewNopLogger(), dbm.NewMemDB(), nil, startupConfig.BaseAppOption) + } else { + app = appBuilder.Build(log.NewNopLogger(), dbm.NewMemDB(), nil) + } + if err := app.Load(true); err != nil { + return nil, fmt.Errorf("failed to load app: %w", err) + } + + // create validator set + valSet, err := startupConfig.ValidatorSet() + if err != nil { + return nil, fmt.Errorf("failed to create validator set") + } + + var ( + balances []banktypes.Balance + genAccounts []authtypes.GenesisAccount + ) + for _, ga := range startupConfig.GenesisAccounts { + genAccounts = append(genAccounts, ga.GenesisAccount) + balances = append(balances, banktypes.Balance{Address: ga.GenesisAccount.GetAddress().String(), Coins: ga.Coins}) + } + + genesisState, err := GenesisStateWithValSet(codec, appBuilder.DefaultGenesis(), valSet, genAccounts, balances...) + if err != nil { + return nil, fmt.Errorf("failed to create genesis state: %w", err) + } + + // init chain must be called to stop deliverState from being nil + stateBytes, err := tmjson.MarshalIndent(genesisState, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal default genesis state: %w", err) + } + + // init chain will set the validator set and initialize the genesis accounts + app.InitChain( + abci.RequestInitChain{ + Validators: []abci.ValidatorUpdate{}, + ConsensusParams: DefaultConsensusParams, + AppStateBytes: stateBytes, + }, + ) + + // commit genesis changes + if !startupConfig.AtGenesis { + app.Commit() + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{ + Height: app.LastBlockHeight() + 1, + AppHash: app.LastCommitID().Hash, + ValidatorsHash: valSet.Hash(), + NextValidatorsHash: valSet.Hash(), + }}) + } + + return app, nil +} + +// GenesisStateWithValSet returns a new genesis state with the validator set +func GenesisStateWithValSet( + codec codec.Codec, + genesisState map[string]json.RawMessage, + valSet *tmtypes.ValidatorSet, + genAccs []authtypes.GenesisAccount, + balances ...banktypes.Balance, +) (map[string]json.RawMessage, error) { + // set genesis accounts + authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) + genesisState[authtypes.ModuleName] = codec.MustMarshalJSON(authGenesis) + + validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) + delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) + + bondAmt := sdk.DefaultPowerReduction + + for _, val := range valSet.Validators { + pk, err := cryptocodec.FromTmPubKeyInterface(val.PubKey) + if err != nil { + return nil, fmt.Errorf("failed to convert pubkey: %w", err) + } + + pkAny, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return nil, fmt.Errorf("failed to create new any: %w", err) + } + + validator := stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress(val.Address).String(), + ConsensusPubkey: pkAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: bondAmt, + DelegatorShares: math.LegacyOneDec(), + Description: stakingtypes.Description{}, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: stakingtypes.NewCommission(math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()), + MinSelfDelegation: math.ZeroInt(), + } + validators = append(validators, validator) + delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), math.LegacyOneDec())) + + } + + // set validators and delegations + stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations) + genesisState[stakingtypes.ModuleName] = codec.MustMarshalJSON(stakingGenesis) + + totalSupply := sdk.NewCoins() + for _, b := range balances { + // add genesis acc tokens to total supply + totalSupply = totalSupply.Add(b.Coins...) + } + + for range delegations { + // add delegated tokens to total supply + totalSupply = totalSupply.Add(sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)) + } + + // add bonded amount to bonded pool module account + balances = append(balances, banktypes.Balance{ + Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), + Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)}, + }) + + // update total supply + bankGenesis := banktypes.NewGenesisState(banktypes.DefaultGenesisState().Params, balances, totalSupply, []banktypes.Metadata{}, []banktypes.SendEnabled{}) + genesisState[banktypes.ModuleName] = codec.MustMarshalJSON(bankGenesis) + + return genesisState, nil +} + +// EmptyAppOptions is a stub implementing AppOptions +type EmptyAppOptions struct{} + +// Get implements AppOptions +func (ao EmptyAppOptions) Get(o string) interface{} { + return nil +} + +// AppOptionsMap is a stub implementing AppOptions which can get data from a map +type AppOptionsMap map[string]interface{} + +func (m AppOptionsMap) Get(key string) interface{} { + v, ok := m[key] + if !ok { + return interface{}(nil) + } + + return v +} + +func NewAppOptionsWithFlagHome(homePath string) servertypes.AppOptions { + return AppOptionsMap{ + flags.FlagHome: homePath, + } +} diff --git a/x/distribution/keeper/delegation.go b/x/distribution/keeper/delegation.go index 3b158c15585e..4df0ff10d158 100644 --- a/x/distribution/keeper/delegation.go +++ b/x/distribution/keeper/delegation.go @@ -3,8 +3,13 @@ package keeper import ( "fmt" +<<<<<<< HEAD sdk "github.com/cosmos/cosmos-sdk/types" +======= + "cosmossdk.io/math" +>>>>>>> c1c23a75d (fix: ensure withdraw_rewards events are always emitted on reward withdrawal (#13323)) + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -162,13 +167,13 @@ func (k Keeper) withdrawDelegationRewards(ctx sdk.Context, val stakingtypes.Vali ) } - // truncate coins, return remainder to community pool - coins, remainder := rewards.TruncateDecimal() + // truncate reward dec coins, return remainder to community pool + finalRewards, remainder := rewards.TruncateDecimal() // add coins to user account - if !coins.IsZero() { + if !finalRewards.IsZero() { withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr()) - err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, coins) + err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, withdrawAddr, finalRewards) if err != nil { return nil, err } @@ -189,5 +194,24 @@ func (k Keeper) withdrawDelegationRewards(ctx sdk.Context, val stakingtypes.Vali // remove delegator starting info k.DeleteDelegatorStartingInfo(ctx, del.GetValidatorAddr(), del.GetDelegatorAddr()) - return coins, nil + if finalRewards.IsZero() { + baseDenom, _ := sdk.GetBaseDenom() + if baseDenom == "" { + baseDenom = sdk.DefaultBondDenom + } + + // Note, we do not call the NewCoins constructor as we do not want the zero + // coin removed. + finalRewards = sdk.Coins{sdk.NewCoin(baseDenom, math.ZeroInt())} + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeWithdrawRewards, + sdk.NewAttribute(sdk.AttributeKeyAmount, finalRewards.String()), + sdk.NewAttribute(types.AttributeKeyValidator, val.GetOperator().String()), + ), + ) + + return finalRewards, nil } diff --git a/x/distribution/keeper/delegation_test.go b/x/distribution/keeper/delegation_test.go index 7b18c0241f3b..6b85e7b6c557 100644 --- a/x/distribution/keeper/delegation_test.go +++ b/x/distribution/keeper/delegation_test.go @@ -6,7 +6,13 @@ import ( "github.com/stretchr/testify/require" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" +<<<<<<< HEAD "github.com/cosmos/cosmos-sdk/simapp" +======= + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/testutil" +>>>>>>> c1c23a75d (fix: ensure withdraw_rewards events are always emitted on reward withdrawal (#13323)) sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/bank/testutil" "github.com/cosmos/cosmos-sdk/x/staking" @@ -684,6 +690,7 @@ func Test100PercentCommissionReward(t *testing.T) { rewards, err := app.DistrKeeper.WithdrawDelegationRewards(ctx, sdk.AccAddress(valAddrs[0]), valAddrs[0]) require.NoError(t, err) +<<<<<<< HEAD denom, _ := sdk.GetBaseDenom() zeroRewards := sdk.Coins{ sdk.Coin{ @@ -691,12 +698,17 @@ func Test100PercentCommissionReward(t *testing.T) { Amount: sdk.ZeroInt(), }, } +======= + zeroRewards := sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, math.ZeroInt())} +>>>>>>> c1c23a75d (fix: ensure withdraw_rewards events are always emitted on reward withdrawal (#13323)) require.True(t, rewards.IsEqual(zeroRewards)) + events := ctx.EventManager().Events() lastEvent := events[len(events)-1] - hasValue := false + + var hasValue bool for _, attr := range lastEvent.Attributes { - if string(attr.Key) == "amount" && string(attr.Value) == "0" { + if attr.Key == "amount" && attr.Value == "0stake" { hasValue = true } } diff --git a/x/distribution/keeper/keeper.go b/x/distribution/keeper/keeper.go index 6af1ede7e5cd..f7cecb9da989 100644 --- a/x/distribution/keeper/keeper.go +++ b/x/distribution/keeper/keeper.go @@ -96,6 +96,7 @@ func (k Keeper) WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddres return nil, err } +<<<<<<< HEAD if rewards.IsZero() { baseDenom, _ := sdk.GetBaseDenom() rewards = sdk.Coins{sdk.Coin{ @@ -112,6 +113,8 @@ func (k Keeper) WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddres ), ) +======= +>>>>>>> c1c23a75d (fix: ensure withdraw_rewards events are always emitted on reward withdrawal (#13323)) // reinitialize the delegation k.initializeDelegation(ctx, valAddr, delAddr) return rewards, nil