Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set validator minimum commissions to at least 5% in upgrade handler #1761

Merged
merged 8 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (community) [#1706] Add disable inflation upgrade to begin blocker that updates x/mint and x/kavadist params
- (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
- (staking) [#1761] Set validator minimum commission to 5% for all validators under 5%

## [v0.24.0]

Expand Down Expand Up @@ -295,6 +296,7 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md).
- [#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run
large-scale simulations remotely using aws-batch

[#1761]: https://github.com/Kava-Labs/kava/pull/1761
[#1752]: https://github.com/Kava-Labs/kava/pull/1752
[#1729]: https://github.com/Kava-Labs/kava/pull/1729
[#1745]: https://github.com/Kava-Labs/kava/pull/1745
Expand Down
74 changes: 74 additions & 0 deletions app/upgrades.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -109,6 +113,8 @@ func upgradeHandler(
return toVM, err
}

UpdateValidatorMinimumCommission(ctx, app)

app.communityKeeper.SetParams(ctx, communityParams)
app.Logger().Info(
"initialized x/community params",
Expand All @@ -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")
drklee3 marked this conversation as resolved.
Show resolved Hide resolved
}

// Set minimum commission rate to 5%, when commission is < 5%
val.Commission.Rate = ValidatorMinimumCommission
drklee3 marked this conversation as resolved.
Show resolved Hide resolved
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)
drklee3 marked this conversation as resolved.
Show resolved Hide resolved

// 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,
)
}
141 changes: 141 additions & 0 deletions app/upgrades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
103 changes: 103 additions & 0 deletions tests/e2e/e2e_upgrade_min_commission_test.go
Original file line number Diff line number Diff line change
@@ -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,
)
}
})
}