Skip to content

Commit

Permalink
feat: set validator minimum commissions to at least 5% in upgrade han…
Browse files Browse the repository at this point in the history
…dler (#1761)

* Update validator min commission in upgrade

* Add min commission upgrade test

* Update changelog

* Set validator MaxRate, call BeforeValidatorModified hook

* Check max commission and update time in tests

* Update e2e test for max rate

* Test val update time

* Use SdkBlock instead of Block
  • Loading branch information
drklee3 committed Oct 31, 2023
1 parent 8e34a60 commit b58577d
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
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")
}

// 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,
)
}
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,
)
}
})
}

0 comments on commit b58577d

Please sign in to comment.