diff --git a/app/upgrades.go b/app/upgrades.go index cef00825..348ce175 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -1,3 +1,265 @@ package app -func (app App) RegisterUpgradeHandlers() {} +import ( + "fmt" + "time" + + sdkmath "cosmossdk.io/math" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + 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" + kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" +) + +const ( + UpgradeName_Mainnet = "v0.25.0" + UpgradeName_Testnet = "v0.25.0-alpha.0" + UpgradeName_E2ETest = "v0.25.0-testing" +) + +var ( + // KAVA to ukava - 6 decimals + kavaConversionFactor = sdk.NewInt(1000_000) + secondsPerYear = sdk.NewInt(365 * 24 * 60 * 60) + + // 10 Million KAVA per year in staking rewards, inflation disable time 2024-01-01T00:00:00 UTC + CommunityParams_Mainnet = communitytypes.NewParams( + time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + // before switchover + sdkmath.LegacyZeroDec(), + // after switchover - 10M KAVA to ukava per year / seconds per year + sdkmath.LegacyNewDec(10_000_000). + MulInt(kavaConversionFactor). + QuoInt(secondsPerYear), + ) + + // Testnet -- 15 Trillion KAVA per year in staking rewards, inflation disable time 2023-11-16T00:00:00 UTC + CommunityParams_Testnet = communitytypes.NewParams( + time.Date(2023, 11, 16, 0, 0, 0, 0, time.UTC), + // before switchover + sdkmath.LegacyZeroDec(), + // after switchover + sdkmath.LegacyNewDec(15_000_000). + MulInt64(1_000_000). // 15M * 1M = 15T + MulInt(kavaConversionFactor). + QuoInt(secondsPerYear), + ) + + CommunityParams_E2E = communitytypes.NewParams( + time.Now().Add(10*time.Second).UTC(), // relative time for testing + 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. +func (app App) RegisterUpgradeHandlers() { + app.upgradeKeeper.SetUpgradeHandler( + UpgradeName_Mainnet, + upgradeHandler(app, UpgradeName_Mainnet, CommunityParams_Mainnet), + ) + app.upgradeKeeper.SetUpgradeHandler( + UpgradeName_Testnet, + upgradeHandler(app, UpgradeName_Testnet, CommunityParams_Testnet), + ) + app.upgradeKeeper.SetUpgradeHandler( + UpgradeName_E2ETest, + upgradeHandler(app, UpgradeName_Testnet, CommunityParams_E2E), + ) + + upgradeInfo, err := app.upgradeKeeper.ReadUpgradeInfoFromDisk() + if err != nil { + panic(err) + } + + doUpgrade := upgradeInfo.Name == UpgradeName_Mainnet || + upgradeInfo.Name == UpgradeName_Testnet || + upgradeInfo.Name == UpgradeName_E2ETest + + if doUpgrade && !app.upgradeKeeper.IsSkipHeight(upgradeInfo.Height) { + storeUpgrades := storetypes.StoreUpgrades{ + Added: []string{ + // x/community added store + communitytypes.ModuleName, + }, + } + + // configure store loader that checks if version == upgradeHeight and applies store upgrades + app.SetStoreLoader(upgradetypes.UpgradeStoreLoader(upgradeInfo.Height, &storeUpgrades)) + } +} + +// upgradeHandler returns an UpgradeHandler for the given upgrade parameters. +func upgradeHandler( + app App, + name string, + communityParams communitytypes.Params, +) upgradetypes.UpgradeHandler { + return func( + ctx sdk.Context, + plan upgradetypes.Plan, + fromVM module.VersionMap, + ) (module.VersionMap, error) { + app.Logger().Info(fmt.Sprintf("running %s upgrade handler", name)) + + toVM, err := app.mm.RunMigrations(ctx, app.configurator, fromVM) + if err != nil { + return toVM, err + } + + // + // Staking validator minimum commission + // + UpdateValidatorMinimumCommission(ctx, app) + + // + // Community Params + // + app.communityKeeper.SetParams(ctx, communityParams) + app.Logger().Info( + "initialized x/community params", + "UpgradeTimeDisableInflation", communityParams.UpgradeTimeDisableInflation, + "StakingRewardsPerSecond", communityParams.StakingRewardsPerSecond, + "UpgradeTimeSetStakingRewardsPerSecond", communityParams.UpgradeTimeSetStakingRewardsPerSecond, + ) + + // + // Kavadist gov grant + // + msgGrant, err := authz.NewMsgGrant( + app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName), // granter + app.accountKeeper.GetModuleAddress(govtypes.ModuleName), // grantee + authz.NewGenericAuthorization(sdk.MsgTypeURL(&banktypes.MsgSend{})), // authorization + nil, // expiration + ) + if err != nil { + return toVM, err + } + _, err = app.authzKeeper.Grant(ctx, msgGrant) + if err != nil { + return toVM, err + } + app.Logger().Info("created gov grant for kavadist funds") + + // + // Gov Quorum + // + govTallyParams := app.govKeeper.GetTallyParams(ctx) + oldQuorum := govTallyParams.Quorum + govTallyParams.Quorum = sdkmath.LegacyMustNewDecFromStr("0.2").String() + app.govKeeper.SetTallyParams(ctx, govTallyParams) + app.Logger().Info(fmt.Sprintf("updated tally quorum from %s to %s", oldQuorum, govTallyParams.Quorum)) + + // + // Incentive Params + // + UpdateIncentiveParams(ctx, app) + + 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, + ) +} + +// UpdateIncentiveParams modifies the earn rewards period for bkava to be 600K KAVA per year. +func UpdateIncentiveParams( + ctx sdk.Context, + app App, +) { + incentiveParams := app.incentiveKeeper.GetParams(ctx) + + // bkava annualized rewards: 600K KAVA + newAmount := sdkmath.LegacyNewDec(600_000). + MulInt(kavaConversionFactor). + QuoInt(secondsPerYear). + TruncateInt() + + for i := range incentiveParams.EarnRewardPeriods { + if incentiveParams.EarnRewardPeriods[i].CollateralType != "bkava" { + continue + } + + // Update rewards per second via index + incentiveParams.EarnRewardPeriods[i].RewardsPerSecond = sdk.NewCoins( + sdk.NewCoin("ukava", newAmount), + ) + } + + app.incentiveKeeper.SetParams(ctx, incentiveParams) +} diff --git a/app/upgrades_test.go b/app/upgrades_test.go new file mode 100644 index 00000000..796ea9ac --- /dev/null +++ b/app/upgrades_test.go @@ -0,0 +1,241 @@ +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" + incentivetypes "github.com/kava-labs/kava/x/incentive/types" + "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) { + require.Equal( + t, + sdkmath.LegacyZeroDec().String(), + app.CommunityParams_Mainnet.StakingRewardsPerSecond.String(), + ) + + require.Equal( + t, + // Manually confirmed + "317097.919837645865043125", + app.CommunityParams_Mainnet.UpgradeTimeSetStakingRewardsPerSecond.String(), + "mainnet kava per second should be correct", + ) +} + +func TestUpgradeCommunityParams_Testnet(t *testing.T) { + require.Equal( + t, + sdkmath.LegacyZeroDec().String(), + app.CommunityParams_Testnet.StakingRewardsPerSecond.String(), + ) + + require.Equal( + t, + // Manually confirmed + "475646879756.468797564687975646", + app.CommunityParams_Testnet.UpgradeTimeSetStakingRewardsPerSecond.String(), + "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 TestUpdateIncentiveParams(t *testing.T) { + tApp := app.NewTestApp() + tApp.InitializeFromGenesisStates() + ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()}) + + ik := tApp.GetIncentiveKeeper() + params := ik.GetParams(ctx) + + startPeriod := time.Date(2021, 10, 26, 15, 0, 0, 0, time.UTC) + endPeriod := time.Date(2022, 10, 26, 15, 0, 0, 0, time.UTC) + + params.EarnRewardPeriods = incentivetypes.MultiRewardPeriods{ + incentivetypes.NewMultiRewardPeriod( + true, + "bkava", + startPeriod, + endPeriod, + sdk.NewCoins( + sdk.NewCoin("ukava", sdk.NewInt(159459)), + ), + ), + } + ik.SetParams(ctx, params) + + beforeParams := ik.GetParams(ctx) + require.Equal(t, params, beforeParams, "initial incentive params should be set") + + // -- UPGRADE + app.UpdateIncentiveParams(ctx, tApp.App) + + // -- After + afterParams := ik.GetParams(ctx) + + require.Len( + t, + afterParams.EarnRewardPeriods[0].RewardsPerSecond, + 1, + "bkava earn reward period should only contain 1 coin", + ) + require.Equal( + t, + // Manual calculation of + // 600,000 * 1000,000 / (365 * 24 * 60 * 60) + sdk.NewCoin("ukava", sdkmath.NewInt(19025)), + afterParams.EarnRewardPeriods[0].RewardsPerSecond[0], + "bkava earn reward period should be updated", + ) + + // Check that other params are not changed + afterParams.EarnRewardPeriods[0].RewardsPerSecond[0] = beforeParams.EarnRewardPeriods[0].RewardsPerSecond[0] + require.Equal( + t, + beforeParams, + afterParams, + "other param values should not be changed", + ) +} + +func generateConsKey( + t *testing.T, +) *ethsecp256k1.PrivKey { + t.Helper() + + key, err := ethsecp256k1.GenerateKey() + require.NoError(t, err) + + return key +} diff --git a/tests/e2e/.env b/tests/e2e/.env index fbd211dd..97050349 100644 --- a/tests/e2e/.env +++ b/tests/e2e/.env @@ -19,14 +19,14 @@ E2E_SKIP_SHUTDOWN=false # The following variables should be defined to run an upgrade. # E2E_INCLUDE_AUTOMATED_UPGRADE when true enables the automated upgrade & corresponding tests in the suite. -E2E_INCLUDE_AUTOMATED_UPGRADE=false +E2E_INCLUDE_AUTOMATED_UPGRADE=true # E2E_KAVA_UPGRADE_NAME is the name of the upgrade that must be in the current local image. -E2E_KAVA_UPGRADE_NAME= +E2E_KAVA_UPGRADE_NAME=v0.25.0-testing # E2E_KAVA_UPGRADE_HEIGHT is the height at which the upgrade will be applied. # If IBC tests are enabled this should be >30. Otherwise, this should be >10. -E2E_KAVA_UPGRADE_HEIGHT= +E2E_KAVA_UPGRADE_HEIGHT=35 # E2E_KAVA_UPGRADE_BASE_IMAGE_TAG is the tag of the docker image the chain should upgrade from. -E2E_KAVA_UPGRADE_BASE_IMAGE_TAG= +E2E_KAVA_UPGRADE_BASE_IMAGE_TAG=v0.24.0 # E2E_KAVA_ERC20_ADDRESS is the address of a pre-deployed ERC20 token with the following properties: # - the E2E_KAVA_FUNDED_ACCOUNT_MNEMONIC has nonzero balance diff --git a/tests/e2e/e2e_upgrade_community_test.go b/tests/e2e/e2e_upgrade_community_test.go new file mode 100644 index 00000000..a68464ea --- /dev/null +++ b/tests/e2e/e2e_upgrade_community_test.go @@ -0,0 +1,256 @@ +package e2e_test + +import ( + "context" + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/tests/util" + communitytypes "github.com/kava-labs/kava/x/community/types" + kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" +) + +func (suite *IntegrationTestSuite) TestUpgradeCommunityParams() { + suite.SkipIfUpgradeDisabled() + + beforeUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight - 1) + afterUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight) + + // Before params + kavaDistParamsBefore, err := suite.Kava.Kavadist.Params(beforeUpgradeCtx, &kavadisttypes.QueryParamsRequest{}) + suite.NoError(err) + mintParamsBefore, err := suite.Kava.Mint.Params(beforeUpgradeCtx, &minttypes.QueryParamsRequest{}) + suite.NoError(err) + + // Before parameters + suite.Run("x/community and x/kavadist parameters before upgrade", func() { + _, err = suite.Kava.Community.Params(beforeUpgradeCtx, &communitytypes.QueryParamsRequest{}) + suite.Error(err, "x/community should not have params before upgrade") + + suite.Require().True( + kavaDistParamsBefore.Params.Active, + "x/kavadist should be active before upgrade", + ) + + suite.Require().True( + mintParamsBefore.Params.InflationMax.IsPositive(), + "x/mint inflation max should be positive before upgrade", + ) + suite.Require().True( + mintParamsBefore.Params.InflationMin.IsPositive(), + "x/mint inflation min should be positive before upgrade", + ) + }) + + // After upgrade, Before switchover - parameters + suite.Run("x/kavadist, x/mint, x/community parameters after upgrade, before switchover", func() { + kavaDistParamsAfter, err := suite.Kava.Kavadist.Params(afterUpgradeCtx, &kavadisttypes.QueryParamsRequest{}) + suite.NoError(err) + mintParamsAfter, err := suite.Kava.Mint.Params(afterUpgradeCtx, &minttypes.QueryParamsRequest{}) + suite.NoError(err) + communityParamsAfter, err := suite.Kava.Community.Params(afterUpgradeCtx, &communitytypes.QueryParamsRequest{}) + suite.NoError(err) + + suite.Equal( + kavaDistParamsBefore.Params, + kavaDistParamsAfter.Params, + "x/kavadist should be unaffected after upgrade", + ) + + suite.Equal( + mintParamsBefore.Params, + mintParamsAfter.Params, + "x/mint params should be unaffected after upgrade", + ) + + expectedParams := app.CommunityParams_E2E + // Make UpgradeTimeDisableInflation match so that we ignore it, because + // referencing app.CommunityParams_E2E in this test files is different + // from the one set in the upgrade handler. At least check that it is + // set to a non-zero value in the assertion below + expectedParams.UpgradeTimeDisableInflation = communityParamsAfter.Params.UpgradeTimeDisableInflation + + suite.False( + communityParamsAfter.Params.UpgradeTimeDisableInflation.IsZero(), + "x/community switchover time should be set after upgrade", + ) + suite.Equal( + expectedParams, + communityParamsAfter.Params, + "x/community params should be set to E2E params after upgrade", + ) + }) + + suite.Require().Eventually( + func() bool { + // Get x/community for switchover time + params, err := suite.Kava.Community.Params( + context.Background(), + &communitytypes.QueryParamsRequest{}, + ) + suite.Require().NoError(err) + + // Check that switchover time is set to zero, e.g. switchover happened + return params.Params.UpgradeTimeDisableInflation.Equal(time.Time{}) + }, + 20*time.Second, 1*time.Second, + "switchover should happen and x/community params should be updated", + ) + + // Fetch exact block when inflation stop event emitted + _, switchoverHeight, err := suite.Kava.GetBeginBlockEventsFromQuery( + context.Background(), + fmt.Sprintf( + "%s.%s EXISTS", + communitytypes.EventTypeInflationStop, + communitytypes.AttributeKeyInflationDisableTime, + ), + ) + suite.Require().NoError(err) + suite.Require().NotZero(switchoverHeight) + + beforeSwitchoverCtx := util.CtxAtHeight(switchoverHeight - 1) + afterSwitchoverCtx := util.CtxAtHeight(switchoverHeight) + + suite.Run("x/kavadist, x/mint, x/community parameters after upgrade, after switchover", func() { + kavaDistParamsAfter, err := suite.Kava.Kavadist.Params( + afterSwitchoverCtx, + &kavadisttypes.QueryParamsRequest{}, + ) + suite.NoError(err) + mintParamsAfter, err := suite.Kava.Mint.Params( + afterSwitchoverCtx, + &minttypes.QueryParamsRequest{}, + ) + suite.NoError(err) + communityParamsAfter, err := suite.Kava.Community.Params( + afterSwitchoverCtx, + &communitytypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + suite.False( + kavaDistParamsAfter.Params.Active, + "x/kavadist should be disabled after upgrade", + ) + + suite.True( + mintParamsAfter.Params.InflationMax.IsZero(), + "x/mint inflation max should be zero after switchover", + ) + suite.True( + mintParamsAfter.Params.InflationMin.IsZero(), + "x/mint inflation min should be zero after switchover", + ) + + suite.Equal( + time.Time{}, + communityParamsAfter.Params.UpgradeTimeDisableInflation, + "x/community switchover time should be reset", + ) + + suite.Equal( + communityParamsAfter.Params.UpgradeTimeSetStakingRewardsPerSecond, + communityParamsAfter.Params.StakingRewardsPerSecond, + "x/community staking rewards per second should match upgrade time staking rewards per second", + ) + }) + + suite.Run("x/kavadist, x/distribution, x/community balances after switchover", func() { + // Before balances - community pool fund consolidation + kavaDistBalBefore, err := suite.Kava.Kavadist.Balance( + beforeSwitchoverCtx, + &kavadisttypes.QueryBalanceRequest{}, + ) + suite.NoError(err) + distrBalBefore, err := suite.Kava.Distribution.CommunityPool( + beforeSwitchoverCtx, + &distrtypes.QueryCommunityPoolRequest{}, + ) + suite.NoError(err) + distrBalCoinsBefore, distrDustBefore := distrBalBefore.Pool.TruncateDecimal() + beforeCommPoolBalance, err := suite.Kava.Community.Balance( + beforeSwitchoverCtx, + &communitytypes.QueryBalanceRequest{}, + ) + suite.NoError(err) + + // After balances + kavaDistBalAfter, err := suite.Kava.Kavadist.Balance( + afterSwitchoverCtx, + &kavadisttypes.QueryBalanceRequest{}, + ) + suite.NoError(err) + distrBalAfter, err := suite.Kava.Distribution.CommunityPool( + afterSwitchoverCtx, + &distrtypes.QueryCommunityPoolRequest{}, + ) + suite.NoError(err) + afterCommPoolBalance, err := suite.Kava.Community.Balance( + afterSwitchoverCtx, + &communitytypes.QueryBalanceRequest{}, + ) + suite.NoError(err) + + expectedKavadistBal := sdk.NewCoins(sdk.NewCoin( + "ukava", + kavaDistBalBefore.Coins.AmountOf("ukava"), + )) + suite.Equal( + expectedKavadistBal, + kavaDistBalAfter.Coins, + "x/kavadist balance should persist the ukava amount and move all other funds", + ) + expectedKavadistTransferred := kavaDistBalBefore.Coins.Sub(expectedKavadistBal...) + + // very low ukava balance after (ignoring dust in x/distribution) + // a small amount of tx fees can still end up here. + // dust should stay in x/distribution, but may not be the same so it's unchecked + distrCoinsAfter, distrDustAfter := distrBalAfter.Pool.TruncateDecimal() + suite.Empty(distrCoinsAfter, "expected no coins in x/distribution community pool") + + // Fetch block results for paid staking rewards in the block + blockRes, err := suite.Kava.TmSignClient.BlockResults( + context.Background(), + &switchoverHeight, + ) + suite.Require().NoError(err) + + stakingRewardPaidEvents := util.FilterEventsByType( + blockRes.BeginBlockEvents, + communitytypes.EventTypeStakingRewardsPaid, + ) + suite.Require().Len(stakingRewardPaidEvents, 1, "there should be only 1 staking reward paid event") + stakingRewardAmount := sdk.NewCoins() + for _, attr := range stakingRewardPaidEvents[0].Attributes { + if string(attr.Key) == communitytypes.AttributeKeyStakingRewardAmount { + stakingRewardAmount, err = sdk.ParseCoinsNormalized(string(attr.Value)) + suite.Require().NoError(err) + + break + } + } + + expectedCommunityBal := beforeCommPoolBalance.Coins. + Add(distrBalCoinsBefore...). + Add(expectedKavadistTransferred...). + Sub(stakingRewardAmount...) // Remove staking rewards paid in the block + + // x/kavadist and x/distribution community pools should be moved to x/community + suite.Equal( + expectedCommunityBal, + afterCommPoolBalance.Coins, + ) + + suite.Equal( + distrDustBefore, + distrDustAfter, + "x/distribution community pool dust should be unchanged", + ) + }) +} diff --git a/tests/e2e/e2e_upgrade_gov_and_authz_test.go b/tests/e2e/e2e_upgrade_gov_and_authz_test.go new file mode 100644 index 00000000..ecfd3dc3 --- /dev/null +++ b/tests/e2e/e2e_upgrade_gov_and_authz_test.go @@ -0,0 +1,291 @@ +package e2e_test + +import ( + "context" + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + query "github.com/cosmos/cosmos-sdk/types/query" + "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + + "github.com/kava-labs/kava/tests/e2e/testutil" + "github.com/kava-labs/kava/tests/util" +) + +const ( + govModuleAcc = "kava10d07y265gmmuvt4z0w9aw880jnsr700jxh8cq5" + communityModuleAcc = "kava17d2wax0zhjrrecvaszuyxdf5wcu5a0p4qlx3t5" + kavadistModuleAcc = "kava1cj7njkw2g9fqx4e768zc75dp9sks8u9znxrf0w" +) + +func (suite *IntegrationTestSuite) TestGovParamChanges() { + suite.SkipIfUpgradeDisabled() + + beforeUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight - 1) + afterUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight) + + // fetch gov parameters before upgrade + govBeforeParams, err := suite.Kava.Gov.Params(beforeUpgradeCtx, &govv1.QueryParamsRequest{ParamsType: "tallying"}) + suite.Require().NoError(err) + + // assert expected gov quorum before upgrade + suite.NotEqual(govBeforeParams.TallyParams.Quorum, "0.200000000000000000") + + govAfterParams, err := suite.Kava.Gov.Params(afterUpgradeCtx, &govv1.QueryParamsRequest{ParamsType: "tallying"}) + suite.Require().NoError(err) + + // assert expected gov quorum after upgrade + suite.Equal(govAfterParams.TallyParams.Quorum, "0.200000000000000000") + +} + +func (suite *IntegrationTestSuite) TestAuthzParamChanges() { + suite.SkipIfUpgradeDisabled() + + beforeUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight - 1) + afterUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight) + + // fetch authz grants before upgrade + authzBeforeGrants, err := suite.Kava.Authz.Grants(beforeUpgradeCtx, &authz.QueryGrantsRequest{Granter: kavadistModuleAcc, Grantee: govModuleAcc, Pagination: &query.PageRequest{Limit: 1000, CountTotal: true}}) + suite.Require().NoError(err) + suite.Require().Equal(authzBeforeGrants.Pagination.Total, uint64(len(authzBeforeGrants.Grants)), "expected all grants to have been requested") + + // no kavadist -> gov grants + suite.Equal(0, len(authzBeforeGrants.Grants)) + + // fetch authz grants after upgrade + authzAfterGrants, err := suite.Kava.Authz.Grants(afterUpgradeCtx, &authz.QueryGrantsRequest{Granter: kavadistModuleAcc, Grantee: govModuleAcc, Pagination: &query.PageRequest{Limit: 1000, CountTotal: true}}) + suite.Require().NoError(err) + suite.Require().Equal(authzAfterGrants.Pagination.Total, uint64(len(authzAfterGrants.Grants)), "expected all grants to have been requested") + + // one kavadist -> gov grants + suite.Require().Equal(1, len(authzAfterGrants.Grants)) + + grant := authzAfterGrants.Grants[0] + + var authorization authz.Authorization + suite.Kava.EncodingConfig.Marshaler.UnpackAny(grant.Authorization, &authorization) + + genericAuthorization, ok := authorization.(*authz.GenericAuthorization) + suite.Require().True(ok, "expected generic authorization") + + // kavadist allows gov to MsgSend it's funds + suite.Equal(sdk.MsgTypeURL(&banktypes.MsgSend{}), genericAuthorization.Msg) + // no expiration + var expectedExpiration *time.Time + suite.Equal(expectedExpiration, grant.Expiration) +} + +func (suite *IntegrationTestSuite) TestModuleAccountGovTransfers() { + suite.SkipIfUpgradeDisabled() + suite.SkipIfKvtoolDisabled() + + // the module account (authority) that executes the transfers + govAcc := sdk.MustAccAddressFromBech32(govModuleAcc) + + // module accounts for gov transfer test cases + communityAcc := sdk.MustAccAddressFromBech32(communityModuleAcc) + kavadistAcc := sdk.MustAccAddressFromBech32(kavadistModuleAcc) + + testCases := []struct { + name string + sender sdk.AccAddress + receiver sdk.AccAddress + amount sdk.Coin + }{ + { + name: "transfer from community to kavadist for incentive rewards", + sender: communityAcc, + receiver: kavadistAcc, + amount: ukava(100e6), + }, + { + name: "transfer from kavadist to community", + sender: kavadistAcc, + receiver: communityAcc, + amount: ukava(50e6), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // create msg exec for transfer between modules + msg := banktypes.NewMsgSend( + tc.sender, + tc.receiver, + sdk.NewCoins(tc.amount), + ) + execMsg := authz.NewMsgExec(govAcc, []sdk.Msg{msg}) + + // ensure proposal passes + passBlock := suite.submitAndPassProposal([]sdk.Msg{&execMsg}) + transfers := suite.getBankTransferAmountAtBlock(passBlock, tc.sender, tc.receiver) + + suite.Require().Containsf( + transfers, + tc.amount, + "expected transfer of %s to be included in bank transfer events: %s", + tc.amount, + transfers, + ) + }) + } +} + +func (suite *IntegrationTestSuite) submitAndPassProposal(msgs []sdk.Msg) int64 { + govParamsRes, err := suite.Kava.Gov.Params(context.Background(), &govv1.QueryParamsRequest{ + ParamsType: govv1.ParamDeposit, + }) + suite.NoError(err) + + kavaAcc := suite.Kava.GetAccount(testutil.FundedAccountName) + + proposalMsg, err := govv1.NewMsgSubmitProposal( + msgs, + govParamsRes.DepositParams.MinDeposit, + kavaAcc.SdkAddress.String(), + "", + ) + suite.NoError(err) + + gasLimit := 1e6 + fee := ukava(1000) + + req := util.KavaMsgRequest{ + Msgs: []sdk.Msg{proposalMsg}, + GasLimit: uint64(gasLimit), + FeeAmount: sdk.NewCoins(fee), + Memo: "this is a proposal please accept me", + } + res := kavaAcc.SignAndBroadcastKavaTx(req) + suite.Require().NoError(res.Err) + + // Wait for proposal to be submitted + txRes, err := util.WaitForSdkTxCommit(suite.Kava.Tx, res.Result.TxHash, 6*time.Second) + suite.Require().NoError(err) + + var govRes govv1.MsgSubmitProposalResponse + suite.decodeTxMsgResponse(txRes, &govRes) + + // 2. Vote for proposal from whale account + whale := suite.Kava.GetAccount(testutil.FundedAccountName) + voteMsg := govv1.NewMsgVote( + whale.SdkAddress, + govRes.ProposalId, + govv1.OptionYes, + "", + ) + + voteReq := util.KavaMsgRequest{ + Msgs: []sdk.Msg{voteMsg}, + GasLimit: uint64(gasLimit), + FeeAmount: sdk.NewCoins(fee), + Memo: "voting", + } + voteRes := whale.SignAndBroadcastKavaTx(voteReq) + suite.Require().NoError(voteRes.Err) + + _, err = util.WaitForSdkTxCommit(suite.Kava.Tx, voteRes.Result.TxHash, 6*time.Second) + suite.Require().NoError(err) + + // 3. Wait until proposal passes + suite.Require().Eventually(func() bool { + proposalRes, err := suite.Kava.Gov.Proposal(context.Background(), &govv1.QueryProposalRequest{ + ProposalId: govRes.ProposalId, + }) + suite.NoError(err) + + switch status := proposalRes.Proposal.Status; status { + case govv1.StatusDepositPeriod, govv1.StatusVotingPeriod: + return false + case govv1.StatusPassed: + return true + case govv1.StatusFailed, govv1.StatusRejected: + suite.Failf("proposal failed", "proposal failed with status %s", status.String()) + return true + } + + return false + }, 60*time.Second, 1*time.Second) + + page := 1 + perPage := 100 + + // Get the block the proposal was passed in + passBlock, err := suite.Kava.TmSignClient.BlockSearch( + context.Background(), + fmt.Sprintf( + "active_proposal.proposal_result = 'proposal_passed' AND active_proposal.proposal_id = %d", + govRes.ProposalId, + ), + &page, + &perPage, + "asc", + ) + suite.Require().NoError(err) + suite.Require().Equal(1, len(passBlock.Blocks), "passed proposal should be searchable") + + return passBlock.Blocks[len(passBlock.Blocks)-1].Block.Height +} + +// getBankTransferAmountAtBlock returns the amount of coins transferred between +// the given accounts in the block at the given height. Note that this returns +// a slice of sdk.Coin that can contain multiple coins of the SAME denom -- ie. NOT sdk.Coins +func (suite *IntegrationTestSuite) getBankTransferAmountAtBlock( + blockHeight int64, + sender sdk.AccAddress, + receiver sdk.AccAddress, +) []sdk.Coin { + // Fetch block results for paid staking rewards in the block + blockRes, err := suite.Kava.TmSignClient.BlockResults( + context.Background(), + &blockHeight, + ) + suite.Require().NoError(err) + + transferEvents := util.FilterEventsByType( + blockRes.EndBlockEvents, // gov proposals applied in EndBlocker + banktypes.EventTypeTransfer, + ) + suite.Require().NotEmpty(transferEvents, "there should be at least 1 bank transfer event") + + transfers := []sdk.Coin{} + +event: + for _, event := range transferEvents { + if event.Type != banktypes.EventTypeTransfer { + suite.FailNowf( + "unexpected event type %s in block results", + event.Type, + ) + } + + for _, attr := range event.Attributes { + suite.T().Logf("event attr: %s = %s", string(attr.Key), string(attr.Value)) + + if string(attr.Key) == banktypes.AttributeKeyRecipient { + if string(attr.Value) != receiver.String() { + continue event + } + } + + if string(attr.Key) == banktypes.AttributeKeySender { + if string(attr.Value) != sender.String() { + continue event + } + } + + if string(attr.Key) == sdk.AttributeKeyAmount { + amount, err := sdk.ParseCoinNormalized(string(attr.Value)) + suite.Require().NoError(err) + + transfers = append(transfers, amount) + } + } + } + + return transfers +} diff --git a/tests/e2e/e2e_upgrade_incentive_test.go b/tests/e2e/e2e_upgrade_incentive_test.go new file mode 100644 index 00000000..1733f120 --- /dev/null +++ b/tests/e2e/e2e_upgrade_incentive_test.go @@ -0,0 +1,65 @@ +package e2e_test + +import ( + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/tests/util" + incentivetypes "github.com/kava-labs/kava/x/incentive/types" +) + +func (suite *IntegrationTestSuite) TestUpgradeIncentiveParams() { + suite.SkipIfUpgradeDisabled() + + beforeUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight - 1) + afterUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight) + + // Before params + incentiveParamsBefore, err := suite.Kava.Incentive.Params( + beforeUpgradeCtx, + &incentivetypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + incentiveParamsAfter, err := suite.Kava.Incentive.Params( + afterUpgradeCtx, + &incentivetypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + suite.Run("x/incentive parameters before upgrade", func() { + suite.Require().Len( + incentiveParamsBefore.Params.EarnRewardPeriods, + 1, + "x/incentive should have 1 earn reward period before upgrade", + ) + + suite.Require().Equal( + sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(159_459))), + incentiveParamsBefore.Params.EarnRewardPeriods[0].RewardsPerSecond, + ) + }) + + suite.Run("x/incentive parameters after upgrade", func() { + suite.Require().Len( + incentiveParamsAfter.Params.EarnRewardPeriods, + 1, + "x/incentive should have 1 earn reward period before upgrade", + ) + + suite.Require().Equal( + // Manual calculation of + // 600,000 * 1000,000 / (365 * 24 * 60 * 60) + sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(19025))), + incentiveParamsAfter.Params.EarnRewardPeriods[0].RewardsPerSecond, + ) + + // No other changes + incentiveParamsAfter.Params.EarnRewardPeriods[0].RewardsPerSecond = incentiveParamsBefore.Params.EarnRewardPeriods[0].RewardsPerSecond + suite.Require().Equal( + incentiveParamsBefore, + incentiveParamsAfter, + "other param values should not be changed", + ) + }) +} diff --git a/tests/e2e/e2e_upgrade_inflation_test.go b/tests/e2e/e2e_upgrade_inflation_test.go new file mode 100644 index 00000000..1ea32ffc --- /dev/null +++ b/tests/e2e/e2e_upgrade_inflation_test.go @@ -0,0 +1,474 @@ +package e2e_test + +import ( + "context" + "fmt" + "strconv" + "time" + + sdkmath "cosmossdk.io/math" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + abci "github.com/tendermint/tendermint/abci/types" + coretypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/kava-labs/kava/tests/util" + communitytypes "github.com/kava-labs/kava/x/community/types" + kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" +) + +func (suite *IntegrationTestSuite) TestUpgradeInflation_Disable() { + suite.SkipIfUpgradeDisabled() + + afterUpgradeCtx := util.CtxAtHeight(suite.UpgradeHeight) + + // Get x/community for switchover time + params, err := suite.Kava.Community.Params(afterUpgradeCtx, &communitytypes.QueryParamsRequest{}) + suite.Require().NoError(err) + + // Sleep until switchover time + 6 seconds for extra block + sleepDuration := time.Until(params.Params.UpgradeTimeDisableInflation.Add(6 * time.Second)) + time.Sleep(sleepDuration) + + suite.Require().Eventually(func() bool { + communityParams, err := suite.Kava.Community.Params(afterUpgradeCtx, &communitytypes.QueryParamsRequest{}) + suite.Require().NoError(err) + + // After params are set in x/community -- non-zero switchover time + return !communityParams.Params.UpgradeTimeDisableInflation.Equal(time.Time{}) + }, 20*time.Second, 3*time.Second) + + // Fetch exact block when inflation stop event emitted + // This is run after the switchover, so we don't need to poll + _, switchoverHeight, err := suite.Kava.GetBeginBlockEventsFromQuery( + context.Background(), + fmt.Sprintf( + "%s.%s EXISTS", + communitytypes.EventTypeInflationStop, + communitytypes.AttributeKeyInflationDisableTime, + ), + ) + suite.Require().NoError(err) + suite.Require().NotZero(switchoverHeight) + + // 1 block before switchover + beforeSwitchoverCtx := util.CtxAtHeight(switchoverHeight - 1) + afterSwitchoverCtx := util.CtxAtHeight(switchoverHeight) + + suite.Run("x/mint, x/kavadist inflation before switchover", func() { + mintParams, err := suite.Kava.Mint.Params( + beforeSwitchoverCtx, + &minttypes.QueryParamsRequest{}, + ) + suite.NoError(err) + kavaDistParams, err := suite.Kava.Kavadist.Params( + beforeSwitchoverCtx, + &kavadisttypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + // Use .String() to compare Decs since x/mint uses the deprecated one, + // mismatch of types but same value. + suite.Equal( + sdkmath.LegacyMustNewDecFromStr("0.595000000000000000").String(), + mintParams.Params.InflationMin.String(), + "x/mint inflation min should be 59.5%% before switchover", + ) + suite.Equal( + sdkmath.LegacyMustNewDecFromStr("0.595000000000000000").String(), + mintParams.Params.InflationMax.String(), + "x/mint inflation max should be 59.5%% before switchover", + ) + + suite.True( + kavaDistParams.Params.Active, + "x/kavadist should be active before switchover", + ) + }) + + suite.Run("x/distribution community tax before switchover", func() { + distrParams, err := suite.Kava.Distribution.Params( + beforeSwitchoverCtx, + &distributiontypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + suite.Equal( + sdkmath.LegacyMustNewDecFromStr("0.949500000000000000").String(), + distrParams.Params.CommunityTax.String(), + "x/distribution community tax should be 94.95%% before switchover", + ) + }) + + suite.Run("x/mint, x/kavadist inflation after switchover", func() { + mintParams, err := suite.Kava.Mint.Params( + afterSwitchoverCtx, + &minttypes.QueryParamsRequest{}, + ) + suite.NoError(err) + kavaDistParams, err := suite.Kava.Kavadist.Params( + afterSwitchoverCtx, + &kavadisttypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + suite.Equal( + sdkmath.LegacyZeroDec().String(), + mintParams.Params.InflationMin.String(), + "x/mint inflation min should be 0% after switchover", + ) + suite.Equal( + sdkmath.LegacyZeroDec().String(), + mintParams.Params.InflationMax.String(), + "x/mint inflation max should be 0% after switchover", + ) + + suite.False( + kavaDistParams.Params.Active, + "x/kavadist should be inactive after switchover", + ) + }) + + suite.Run("x/distribution community tax after switchover", func() { + distrParams, err := suite.Kava.Distribution.Params( + afterSwitchoverCtx, + &distributiontypes.QueryParamsRequest{}, + ) + suite.NoError(err) + + suite.Equal( + sdkmath.LegacyZeroDec().String(), + distrParams.Params.CommunityTax.String(), + "x/distribution community tax should be 0%% before switchover", + ) + }) + + // Ensure inflation was still active before switchover + suite.Run("positive mint events before switchover", func() { + // 1 block before switchover + queryHeight := switchoverHeight - 1 + + block, err := suite.Kava.TmSignClient.BlockResults( + context.Background(), + &queryHeight, + ) + suite.Require().NoError(err) + + // Mint events should only occur in begin block + mintEvents := util.FilterEventsByType(block.BeginBlockEvents, minttypes.EventTypeMint) + + suite.Require().NotEmpty(mintEvents, "mint events should be emitted") + + // Ensure mint amounts are non-zero + found := false + for _, event := range mintEvents { + for _, attribute := range event.Attributes { + // Bonded ratio and annual provisions unchecked + + if string(attribute.Key) == minttypes.AttributeKeyInflation { + suite.Equal( + sdkmath.LegacyMustNewDecFromStr("0.595000000000000000").String(), + string(attribute.Value), + "inflation should be 59.5%% before switchover", + ) + } + + if string(attribute.Key) == sdk.AttributeKeyAmount { + found = true + // Parse as native go int, not necessary to use sdk.Int + value, err := strconv.Atoi(string(attribute.Value)) + suite.Require().NoError(err) + + suite.NotZero(value, "mint amount should be non-zero") + suite.Positive(value, "mint amount should be positive") + } + } + } + + suite.True(found, "mint amount should be found") + }) + + suite.Run("staking denom supply increases before switchover", func() { + queryHeight := switchoverHeight - 2 + + supply1, err := suite.Kava.Bank.SupplyOf( + util.CtxAtHeight(queryHeight), + &types.QuerySupplyOfRequest{ + Denom: suite.Kava.StakingDenom, + }, + ) + suite.Require().NoError(err) + + suite.NotZero(supply1.Amount, "ukava supply should be non-zero") + + // Next block + queryHeight += 1 + supply2, err := suite.Kava.Bank.SupplyOf( + util.CtxAtHeight(queryHeight), + &types.QuerySupplyOfRequest{ + Denom: suite.Kava.StakingDenom, + }, + ) + suite.Require().NoError(err) + + suite.NotZero(supply2.Amount, "ukava supply should be non-zero") + + suite.Truef( + supply2.Amount.Amount.GT(supply1.Amount.Amount), + "ukava supply before switchover should increase between blocks, %s > %s", + supply2.Amount.Amount.String(), + ) + }) + + // Check if inflation is ACTUALLY disabled... check if any coins are being + // minted in the blocks after switchover + suite.Run("no minting after switchover", func() { + kavaSupply := sdk.NewCoin(suite.Kava.StakingDenom, sdkmath.ZeroInt()) + + // Next 5 blocks after switchover, ensure there's actually no more inflation + for i := 0; i < 5; i++ { + queryHeight := switchoverHeight + int64(i) + + suite.Run( + fmt.Sprintf("x/mint events with 0 amount @ height=%d", queryHeight), + func() { + var block *coretypes.ResultBlockResults + suite.Require().Eventually(func() bool { + // Check begin block events + block, err = suite.Kava.TmSignClient.BlockResults( + context.Background(), + &queryHeight, + ) + + return err == nil + }, 20*time.Second, 3*time.Second) + + var mintEvents []abci.Event + + // Mint events should only occur in begin block, but we just include + // everything else just in case anything changes in x/mint + mintEventsBegin := util.FilterEventsByType(block.BeginBlockEvents, minttypes.EventTypeMint) + mintEventsEnd := util.FilterEventsByType(block.EndBlockEvents, minttypes.EventTypeMint) + mintEventsTx := util.FilterTxEventsByType(block.TxsResults, minttypes.EventTypeMint) + + mintEvents = append(mintEvents, mintEventsBegin...) + mintEvents = append(mintEvents, mintEventsEnd...) + mintEvents = append(mintEvents, mintEventsTx...) + + suite.Require().NotEmpty(mintEvents, "mint events should still be emitted") + + // Ensure mint amounts are 0 + found := false + for _, event := range mintEvents { + for _, attribute := range event.Attributes { + // Bonded ratio and annual provisions unchecked + + if string(attribute.Key) == minttypes.AttributeKeyInflation { + suite.Equal(sdkmath.LegacyZeroDec().String(), string(attribute.Value)) + } + + if string(attribute.Key) == sdk.AttributeKeyAmount { + found = true + suite.Equal(sdkmath.ZeroInt().String(), string(attribute.Value)) + } + } + } + + suite.True(found, "mint amount should be found") + }, + ) + + // Run this after the events check, since that one waits for the + // new block if necessary + suite.Run( + fmt.Sprintf("total staking denom supply should not change @ height=%d", queryHeight), + func() { + supplyRes, err := suite.Kava.Bank.SupplyOf( + util.CtxAtHeight(queryHeight), + &types.QuerySupplyOfRequest{ + Denom: suite.Kava.StakingDenom, + }, + ) + suite.Require().NoError(err) + + if kavaSupply.IsZero() { + // First iteration, set supply + kavaSupply = supplyRes.Amount + } else { + suite.Require().Equal( + kavaSupply, + supplyRes.Amount, + "ukava supply should not change", + ) + } + }, + ) + } + }) + + suite.Run("no staking rewards from x/community before switchover", func() { + // 1 block before switchover + queryHeight := switchoverHeight - 1 + + block, err := suite.Kava.TmSignClient.BlockResults( + context.Background(), + &queryHeight, + ) + suite.Require().NoError(err) + + // Events are not emitted if amount is 0 + stakingRewardEvents := util.FilterEventsByType(block.BeginBlockEvents, communitytypes.EventTypeStakingRewardsPaid) + suite.Require().Empty(stakingRewardEvents, "staking reward events should not be emitted") + }) + + suite.Run("staking rewards pay out from x/community after switchover", func() { + for i := 0; i < 5; i++ { + // after switchover + queryHeight := switchoverHeight + int64(i) + + block, err := suite.Kava.TmSignClient.BlockResults( + context.Background(), + &queryHeight, + ) + suite.Require().NoError(err) + + stakingRewardEvents := util.FilterEventsByType( + block.BeginBlockEvents, + communitytypes.EventTypeStakingRewardsPaid, + ) + suite.Require().NotEmptyf( + stakingRewardEvents, + "staking reward events should be emitted at height=%d", + queryHeight, + ) + + // Ensure amounts are non-zero + found := false + for _, attr := range stakingRewardEvents[0].Attributes { + if string(attr.Key) == communitytypes.AttributeKeyStakingRewardAmount { + coins, err := sdk.ParseCoinNormalized(string(attr.Value)) + suite.Require().NoError(err, "staking reward amount should be parsable coins") + + suite.Truef( + coins.Amount.IsPositive(), + "staking reward amount should be a positive amount at height=%d", + queryHeight, + ) + found = true + } + } + + suite.Truef( + found, + "staking reward amount should be found in events at height=%d", + queryHeight, + ) + } + }) + + // Staking rewards can still be claimed + suite.Run("staking rewards claimable after switchover", func() { + suite.SkipIfKvtoolDisabled() + + // Get the delegator of the only validator + validators, err := suite.Kava.Staking.Validators( + context.Background(), + &stakingtypes.QueryValidatorsRequest{}, + ) + suite.Require().NoError(err) + suite.Require().Positive(len(validators.Validators), "should only be at least 1 validator") + + valAddr, err := sdk.ValAddressFromBech32(validators.Validators[0].OperatorAddress) + suite.Require().NoError(err) + + accAddr := sdk.AccAddress(valAddr.Bytes()) + + balBefore, err := suite.Kava.Bank.Balance( + context.Background(), + &types.QueryBalanceRequest{ + Address: accAddr.String(), + Denom: suite.Kava.StakingDenom, + }, + ) + suite.Require().NoError(err) + suite.Require().False(balBefore.Balance.IsZero(), "val staking denom balance should be non-zero") + + delegationRewards, err := suite.Kava.Distribution.DelegationRewards( + context.Background(), + &distributiontypes.QueryDelegationRewardsRequest{ + ValidatorAddress: valAddr.String(), + DelegatorAddress: accAddr.String(), + }, + ) + suite.Require().NoError(err) + + suite.False(delegationRewards.Rewards.Empty()) + suite.True(delegationRewards.Rewards.IsAllPositive(), "queried rewards should be positive") + + withdrawRewardsMsg := distributiontypes.NewMsgWithdrawDelegatorReward( + accAddr, + valAddr, + ) + + // Get the validator private key from kava keyring + key, err := suite.Kava.Keyring.(unsafeExporter).ExportPrivateKeyObject( + "validator", + ) + suite.Require().NoError(err) + + acc := suite.Kava.AddNewSigningAccountFromPrivKey( + "validator", + key, + "", + suite.Kava.ChainID, + ) + + gasLimit := int64(2e5) + fee := ukava(200) + req := util.KavaMsgRequest{ + Msgs: []sdk.Msg{withdrawRewardsMsg}, + GasLimit: uint64(gasLimit), + FeeAmount: sdk.NewCoins(fee), + Memo: "give me my money", + } + res := acc.SignAndBroadcastKavaTx(req) + + _, err = util.WaitForSdkTxCommit(suite.Kava.Tx, res.Result.TxHash, 6*time.Second) + suite.Require().NoError(err) + + balAfter, err := suite.Kava.Bank.Balance( + context.Background(), + &types.QueryBalanceRequest{ + Address: accAddr.String(), + Denom: suite.Kava.StakingDenom, + }, + ) + suite.Require().NoError(err) + suite.Require().False(balAfter.Balance.IsZero(), "val staking denom balance should be non-zero") + + balIncrease := balAfter.Balance. + Sub(*balBefore.Balance). + Add(res.Tx.GetFee()[0]) // Add the fee back to balance to compare actual balances + + queriedRewardsCoins, _ := delegationRewards.Rewards.TruncateDecimal() + + suite.Require().Truef( + queriedRewardsCoins.AmountOf(suite.Kava.StakingDenom). + LTE(balIncrease.Amount), + "claimed rewards should be >= queried delegation rewards, got claimed %s vs queried %s", + balIncrease.Amount.String(), + queriedRewardsCoins.AmountOf(suite.Kava.StakingDenom).String(), + ) + }) +} + +// unsafeExporter is implemented by key stores that support unsafe export +// of private keys' material. +type unsafeExporter interface { + // ExportPrivateKeyObject returns a private key in unarmored format. + ExportPrivateKeyObject(uid string) (cryptotypes.PrivKey, error) +} 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 00000000..38455a95 --- /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/testutil/chain.go b/tests/e2e/testutil/chain.go index 7636fc9c..8217fd40 100644 --- a/tests/e2e/testutil/chain.go +++ b/tests/e2e/testutil/chain.go @@ -14,6 +14,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" txtypes "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + authz "github.com/cosmos/cosmos-sdk/x/authz" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" govv1types "github.com/cosmos/cosmos-sdk/x/gov/types/v1" @@ -39,6 +40,7 @@ import ( communitytypes "github.com/kava-labs/kava/x/community/types" earntypes "github.com/kava-labs/kava/x/earn/types" evmutiltypes "github.com/kava-labs/kava/x/evmutil/types" + incentivetypes "github.com/kava-labs/kava/x/incentive/types" kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" ) @@ -58,11 +60,13 @@ type Chain struct { EncodingConfig kavaparams.EncodingConfig Auth authtypes.QueryClient + Authz authz.QueryClient Bank banktypes.QueryClient Cdp cdptypes.QueryClient Committee committeetypes.QueryClient Community communitytypes.QueryClient Distribution distrtypes.QueryClient + Incentive incentivetypes.QueryClient Kavadist kavadisttypes.QueryClient Earn earntypes.QueryClient Evm evmtypes.QueryClient @@ -120,11 +124,13 @@ func NewChain(t *testing.T, details *runner.ChainDetails, fundedAccountMnemonic } chain.Auth = authtypes.NewQueryClient(grpcConn) + chain.Authz = authz.NewQueryClient(grpcConn) chain.Bank = banktypes.NewQueryClient(grpcConn) chain.Cdp = cdptypes.NewQueryClient(grpcConn) chain.Committee = committeetypes.NewQueryClient(grpcConn) chain.Community = communitytypes.NewQueryClient(grpcConn) chain.Distribution = distrtypes.NewQueryClient(grpcConn) + chain.Incentive = incentivetypes.NewQueryClient(grpcConn) chain.Kavadist = kavadisttypes.NewQueryClient(grpcConn) chain.Earn = earntypes.NewQueryClient(grpcConn) chain.Evm = evmtypes.NewQueryClient(grpcConn) @@ -224,6 +230,21 @@ func (chain *Chain) QuerySdkForBalances(addr sdk.AccAddress) sdk.Coins { return res.Balances } +// QuerySdkForBalancesAtHeight gets the balance of a particular address on this Chain, at the specified height. +func (chain *Chain) QuerySdkForBalancesAtHeight( + height int64, + addr sdk.AccAddress, +) sdk.Coins { + res, err := chain.Bank.AllBalances( + util.CtxAtHeight(height), + &banktypes.QueryAllBalancesRequest{ + Address: addr.String(), + }, + ) + require.NoError(chain.t, err) + return res.Balances +} + // GetModuleBalances returns the balance of a requested module account func (chain *Chain) GetModuleBalances(moduleName string) sdk.Coins { addr := authtypes.NewModuleAddress(moduleName)