diff --git a/CHANGELOG.md b/CHANGELOG.md index 4abe6a0ea..a3bce3ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (community) [#1729] Consolidate community funds from `x/distribution` and `x/kavadist` to `x/community` - (community) [#1752] Set `x/distribution` CommunityTax to zero on inflation disable upgrade - (community) [#1755] Keep funds in `x/community` in `CommunityPoolLendWithdrawProposal` handler - +- (staking) [#1761] Set validator minimum commission to 5% for all validators under 5% ## [v0.24.1](https://github.com/Kava-Labs/kava/releases/tag/v0.24.1) @@ -303,6 +303,7 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md). large-scale simulations remotely using aws-batch [#1755]: https://github.com/Kava-Labs/kava/pull/1755 +[#1761]: https://github.com/Kava-Labs/kava/pull/1761 [#1752]: https://github.com/Kava-Labs/kava/pull/1752 [#1751]: https://github.com/Kava-Labs/kava/pull/1751 [#1745]: https://github.com/Kava-Labs/kava/pull/1745 diff --git a/app/upgrades.go b/app/upgrades.go index bd2f6cc14..3c5b4f5b4 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -8,6 +8,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" communitytypes "github.com/kava-labs/kava/x/community/types" @@ -52,6 +53,9 @@ var ( sdkmath.LegacyNewDec(0), // stakingRewardsPerSecond sdkmath.LegacyNewDec(1000), // upgradeTimeSetstakingRewardsPerSecond ) + + // ValidatorMinimumCommission is the new 5% minimum commission rate for validators + ValidatorMinimumCommission = sdk.NewDecWithPrec(5, 2) ) // RegisterUpgradeHandlers registers the upgrade handlers for the app. @@ -109,6 +113,8 @@ func upgradeHandler( return toVM, err } + UpdateValidatorMinimumCommission(ctx, app) + app.communityKeeper.SetParams(ctx, communityParams) app.Logger().Info( "initialized x/community params", @@ -120,3 +126,71 @@ func upgradeHandler( return toVM, nil } } + +// UpdateValidatorMinimumCommission updates the commission rate for all +// validators to be at least the new min commission rate, and sets the minimum +// commission rate in the staking params. +func UpdateValidatorMinimumCommission( + ctx sdk.Context, + app App, +) { + resultCount := make(map[stakingtypes.BondStatus]int) + + // Iterate over *all* validators including inactive + app.stakingKeeper.IterateValidators( + ctx, + func(index int64, validator stakingtypes.ValidatorI) (stop bool) { + // Skip if validator commission is already >= 5% + if validator.GetCommission().GTE(ValidatorMinimumCommission) { + return false + } + + val, ok := validator.(stakingtypes.Validator) + if !ok { + panic("expected stakingtypes.Validator") + } + + // Set minimum commission rate to 5%, when commission is < 5% + val.Commission.Rate = ValidatorMinimumCommission + val.Commission.UpdateTime = ctx.BlockTime() + + // Update MaxRate if necessary + if val.Commission.MaxRate.LT(ValidatorMinimumCommission) { + val.Commission.MaxRate = ValidatorMinimumCommission + } + + if err := app.stakingKeeper.BeforeValidatorModified(ctx, val.GetOperator()); err != nil { + panic(fmt.Sprintf("failed to call BeforeValidatorModified: %s", err)) + } + app.stakingKeeper.SetValidator(ctx, val) + + // Keep track of counts just for logging purposes + switch val.GetStatus() { + case stakingtypes.Bonded: + resultCount[stakingtypes.Bonded]++ + case stakingtypes.Unbonded: + resultCount[stakingtypes.Unbonded]++ + case stakingtypes.Unbonding: + resultCount[stakingtypes.Unbonding]++ + } + + return false + }, + ) + + app.Logger().Info( + "updated validator minimum commission rate for all existing validators", + stakingtypes.BondStatusBonded, resultCount[stakingtypes.Bonded], + stakingtypes.BondStatusUnbonded, resultCount[stakingtypes.Unbonded], + stakingtypes.BondStatusUnbonding, resultCount[stakingtypes.Unbonding], + ) + + stakingParams := app.stakingKeeper.GetParams(ctx) + stakingParams.MinCommissionRate = ValidatorMinimumCommission + app.stakingKeeper.SetParams(ctx, stakingParams) + + app.Logger().Info( + "updated x/staking params minimum commission rate", + "MinCommissionRate", stakingParams.MinCommissionRate, + ) +} diff --git a/app/upgrades_test.go b/app/upgrades_test.go index 804fe6e47..e66b530d4 100644 --- a/app/upgrades_test.go +++ b/app/upgrades_test.go @@ -2,10 +2,16 @@ package app_test import ( "testing" + "time" sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/evmos/ethermint/crypto/ethsecp256k1" "github.com/kava-labs/kava/app" "github.com/stretchr/testify/require" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" ) func TestUpgradeCommunityParams_Mainnet(t *testing.T) { @@ -39,3 +45,138 @@ func TestUpgradeCommunityParams_Testnet(t *testing.T) { "testnet kava per second should be correct", ) } + +func TestUpdateValidatorMinimumCommission(t *testing.T) { + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()}) + + sk := tApp.GetStakingKeeper() + stakingParams := sk.GetParams(ctx) + stakingParams.MinCommissionRate = sdk.ZeroDec() + sk.SetParams(ctx, stakingParams) + + // Set some validators with varying commission rates + + vals := []struct { + name string + operatorAddr sdk.ValAddress + consPriv *ethsecp256k1.PrivKey + commissionRateMin sdk.Dec + commissionRateMax sdk.Dec + shouldBeUpdated bool + }{ + { + name: "zero commission rate", + operatorAddr: sdk.ValAddress("val0"), + consPriv: generateConsKey(t), + commissionRateMin: sdk.ZeroDec(), + commissionRateMax: sdk.ZeroDec(), + shouldBeUpdated: true, + }, + { + name: "0.01 commission rate", + operatorAddr: sdk.ValAddress("val1"), + consPriv: generateConsKey(t), + commissionRateMin: sdk.MustNewDecFromStr("0.01"), + commissionRateMax: sdk.MustNewDecFromStr("0.01"), + shouldBeUpdated: true, + }, + { + name: "0.05 commission rate", + operatorAddr: sdk.ValAddress("val2"), + consPriv: generateConsKey(t), + commissionRateMin: sdk.MustNewDecFromStr("0.05"), + commissionRateMax: sdk.MustNewDecFromStr("0.05"), + shouldBeUpdated: false, + }, + { + name: "0.06 commission rate", + operatorAddr: sdk.ValAddress("val3"), + consPriv: generateConsKey(t), + commissionRateMin: sdk.MustNewDecFromStr("0.06"), + commissionRateMax: sdk.MustNewDecFromStr("0.06"), + shouldBeUpdated: false, + }, + { + name: "0.5 commission rate", + operatorAddr: sdk.ValAddress("val4"), + consPriv: generateConsKey(t), + commissionRateMin: sdk.MustNewDecFromStr("0.5"), + commissionRateMax: sdk.MustNewDecFromStr("0.5"), + shouldBeUpdated: false, + }, + } + + for _, v := range vals { + val, err := stakingtypes.NewValidator( + v.operatorAddr, + v.consPriv.PubKey(), + stakingtypes.Description{}, + ) + require.NoError(t, err) + val.Commission.Rate = v.commissionRateMin + val.Commission.MaxRate = v.commissionRateMax + + err = sk.SetValidatorByConsAddr(ctx, val) + require.NoError(t, err) + sk.SetValidator(ctx, val) + } + + require.NotPanics( + t, func() { + app.UpdateValidatorMinimumCommission(ctx, tApp.App) + }, + ) + + stakingParamsAfter := sk.GetParams(ctx) + require.Equal(t, stakingParamsAfter.MinCommissionRate, app.ValidatorMinimumCommission) + + // Check that all validators have a commission rate >= 5% + for _, val := range vals { + t.Run(val.name, func(t *testing.T) { + validator, found := sk.GetValidator(ctx, val.operatorAddr) + require.True(t, found, "validator should be found") + + require.True( + t, + validator.GetCommission().GTE(app.ValidatorMinimumCommission), + "commission rate should be >= 5%", + ) + + require.True( + t, + validator.Commission.MaxRate.GTE(app.ValidatorMinimumCommission), + "commission rate max should be >= 5%, got %s", + validator.Commission.MaxRate, + ) + + if val.shouldBeUpdated { + require.Equal( + t, + ctx.BlockTime(), + validator.Commission.UpdateTime, + "commission update time should be set to block time", + ) + } else { + require.Equal( + t, + time.Unix(0, 0).UTC(), + validator.Commission.UpdateTime, + "commission update time should not be changed -- default value is 0", + ) + } + }) + } +} + +func generateConsKey( + t *testing.T, +) *ethsecp256k1.PrivKey { + t.Helper() + + key, err := ethsecp256k1.GenerateKey() + require.NoError(t, err) + + return key +} diff --git a/tests/e2e/e2e_upgrade_min_commission_test.go b/tests/e2e/e2e_upgrade_min_commission_test.go new file mode 100644 index 000000000..38455a951 --- /dev/null +++ b/tests/e2e/e2e_upgrade_min_commission_test.go @@ -0,0 +1,103 @@ +package e2e_test + +import ( + "context" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/grpc/tmservice" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/kava-labs/kava/tests/util" +) + +func (suite *IntegrationTestSuite) TestValMinCommission() { + suite.SkipIfUpgradeDisabled() + + beforeUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight - 1) + afterUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight) + + suite.Run("before upgrade", func() { + // Before params + beforeParams, err := suite.Kava.Staking.Params(beforeUpgradeCtx, &types.QueryParamsRequest{}) + suite.Require().NoError(err) + + suite.Require().Equal( + sdkmath.LegacyZeroDec().String(), + beforeParams.Params.MinCommissionRate.String(), + "min commission rate should be 0%% before upgrade", + ) + + // Before validators + beforeValidators, err := suite.Kava.Staking.Validators(beforeUpgradeCtx, &types.QueryValidatorsRequest{}) + suite.Require().NoError(err) + + for _, val := range beforeValidators.Validators { + // In kvtool gentx, the commission rate is set to 0, with max of 0.01 + expectedRate := sdkmath.LegacyZeroDec() + expectedRateMax := sdkmath.LegacyMustNewDecFromStr("0.01") + + suite.Require().Equalf( + expectedRate.String(), + val.Commission.CommissionRates.Rate.String(), + "validator %s should have commission rate of %s before upgrade", + val.OperatorAddress, + expectedRate, + ) + + suite.Require().Equalf( + expectedRateMax.String(), + val.Commission.CommissionRates.MaxRate.String(), + "validator %s should have max commission rate of %s before upgrade", + val.OperatorAddress, + expectedRateMax, + ) + } + }) + + suite.Run("after upgrade", func() { + block, err := suite.Kava.Tm.GetBlockByHeight(context.Background(), &tmservice.GetBlockByHeightRequest{ + Height: suite.UpgradeHeight, + }) + suite.Require().NoError(err) + + // After params + afterParams, err := suite.Kava.Staking.Params(afterUpgradeCtx, &types.QueryParamsRequest{}) + suite.Require().NoError(err) + + expectedMinRate := sdk.MustNewDecFromStr("0.05") + + suite.Require().Equal( + expectedMinRate.String(), + afterParams.Params.MinCommissionRate.String(), + "min commission rate should be 5%% after upgrade", + ) + + // After validators + afterValidators, err := suite.Kava.Staking.Validators(afterUpgradeCtx, &types.QueryValidatorsRequest{}) + suite.Require().NoError(err) + + for _, val := range afterValidators.Validators { + + suite.Require().Truef( + val.Commission.CommissionRates.Rate.GTE(expectedMinRate), + "validator %s should have commission rate of at least 5%%", + val.OperatorAddress, + ) + + suite.Require().Truef( + val.Commission.CommissionRates.MaxRate.GTE(expectedMinRate), + "validator %s should have max commission rate of at least 5%%", + val.OperatorAddress, + ) + + suite.Require().Truef( + val.Commission.UpdateTime.Equal(block.SdkBlock.Header.Time), + "validator %s should have commission update time set to block time, expected %s, got %s", + val.OperatorAddress, + block.SdkBlock.Header.Time, + val.Commission.UpdateTime, + ) + } + }) +} diff --git a/tests/e2e/kvtool b/tests/e2e/kvtool index bec8a8d82..b7948a9ac 160000 --- a/tests/e2e/kvtool +++ b/tests/e2e/kvtool @@ -1 +1 @@ -Subproject commit bec8a8d82d1b0a2336724c428dbb472a6dd0411e +Subproject commit b7948a9acf002b41b9b730b23bd625f30a9a0902