diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 13523f806..ef35eed21 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -125,6 +125,7 @@ jobs: "Auction", "CellarFees", "Incentives", + "ValidatorIncentives", "Pubsub", "Addresses", ] diff --git a/Makefile b/Makefile index 314f45e1e..da6b73e79 100644 --- a/Makefile +++ b/Makefile @@ -388,6 +388,9 @@ e2e_cellarfees_test: e2e_clean_slate e2e_incentives_test: e2e_clean_slate @E2E_SKIP_CLEANUP=true integration_tests/integration_tests.test -test.failfast -test.v -test.run IntegrationTestSuite -testify.m TestIncentives || make -s fail +e2e_validator_incentives_test: e2e_clean_slate + @E2E_SKIP_CLEANUP=true integration_tests/integration_tests.test -test.failfast -test.v -test.run IntegrationTestSuite -testify.m TestValidatorIncentives || make -s fail + e2e_pubsub_test: e2e_clean_slate @E2E_SKIP_CLEANUP=true integration_tests/integration_tests.test -test.failfast -test.v -test.run IntegrationTestSuite -testify.m TestPubsub || make -s fail diff --git a/app/app.go b/app/app.go index e98fc33a4..4baeea642 100644 --- a/app/app.go +++ b/app/app.go @@ -520,11 +520,7 @@ func NewSommelierApp( ) app.IncentivesKeeper = incentiveskeeper.NewKeeper( - appCodec, keys[incentivestypes.StoreKey], app.GetSubspace(incentivestypes.ModuleName), app.DistrKeeper, app.BankKeeper, app.MintKeeper, - ) - - app.IncentivesKeeper = incentiveskeeper.NewKeeper( - appCodec, keys[incentivestypes.StoreKey], app.GetSubspace(incentivestypes.ModuleName), app.DistrKeeper, app.BankKeeper, app.MintKeeper, + appCodec, keys[incentivestypes.StoreKey], app.GetSubspace(incentivestypes.ModuleName), app.DistrKeeper, app.BankKeeper, app.MintKeeper, app.StakingKeeper, ) app.GravityKeeper = *app.GravityKeeper.SetHooks( diff --git a/integration_tests/genesis.go b/integration_tests/genesis.go index 421ecbcf8..062cee700 100644 --- a/integration_tests/genesis.go +++ b/integration_tests/genesis.go @@ -12,6 +12,8 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/peggyjv/sommelier/v7/app/params" + incentivestypes "github.com/peggyjv/sommelier/v7/x/incentives/types" ) func getGenDoc(path string) (*tmtypes.GenesisDoc, error) { @@ -108,3 +110,17 @@ func addGenesisAccount(path, moniker, amountStr string, accAddr sdk.AccAddress) genDoc.AppState = appStateJSON return genutil.ExportGenesisFile(genDoc, genFile) } + +func (s *IntegrationTestSuite) setIncentivesGenState(appGenState map[string]json.RawMessage) error { + incentivesGenState := incentivestypes.DefaultGenesisState() + err := cdc.UnmarshalJSON(appGenState[incentivestypes.ModuleName], &incentivesGenState) + if err != nil { + return fmt.Errorf("failed to unmarshal incentives genesis state: %w", err) + } + + incentivesGenState.Params.ValidatorIncentivesCutoffHeight = 0 + incentivesGenState.Params.ValidatorMaxDistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.NewInt(0)) + + appGenState[incentivestypes.ModuleName] = cdc.MustMarshalJSON(&incentivesGenState) + return nil +} diff --git a/integration_tests/incentives_test.go b/integration_tests/incentives_test.go index e15c84725..495c2a228 100644 --- a/integration_tests/incentives_test.go +++ b/integration_tests/incentives_test.go @@ -241,3 +241,249 @@ func (s *IntegrationTestSuite) getRewardAmountAndHeight(ctx context.Context, dis return amount, height } + +func (s *IntegrationTestSuite) TestValidatorIncentives() { + validator := s.chain.validators[0] + proposer := s.chain.proposer + proposerCtx, err := s.chain.clientContext("tcp://localhost:26657", proposer.keyring, "proposer", proposer.address()) + s.Require().NoError(err) + orch := s.chain.orchestrators[0] + orchClientCtx, err := s.chain.clientContext("tcp://localhost:26657", orch.keyring, "orch", orch.address()) + s.Require().NoError(err) + ctx := context.Background() + + s.T().Log("Getting the initial community pool balance") + queryClient := disttypes.NewQueryClient(proposerCtx) + queryRes, err := queryClient.CommunityPool(ctx, &disttypes.QueryCommunityPoolRequest{}) + s.Require().NoError(err) + s.T().Logf("Initial community pool balance: %s", queryRes.Pool.String()) + initialCommunityPool := queryRes.Pool + + // Wait for outstanding rewards to equal 4000000usomm. Current theory is these initial rewards come from + // the genesis delegation tx fees. + s.T().Log("Waiting for outstanding rewards to equal 4000000usomm") + initialRewards := disttypes.ValidatorOutstandingRewards{ + Rewards: sdk.DecCoins{ + sdk.DecCoin{ + Denom: params.BaseCoinUnit, + Amount: sdk.NewDec(4000000), + }, + }, + } + s.Require().Eventually(func() bool { + rewards, err := s.getValidatorOutstandingRewards(validator) + s.Require().NoError(err) + return rewards.Rewards.AmountOf(params.BaseCoinUnit).Equal(initialRewards.Rewards.AmountOf(params.BaseCoinUnit)) + }, time.Second*30, time.Second*1, "outstanding rewards did not reach 4000000usomm") + + // Submit proposal to enable validator incentives + s.T().Log("Submitting proposal to enable validator incentives") + cutoffHeight := 100 + proposal := paramsproposal.ParameterChangeProposal{ + Title: "Enable validator incentives", + Description: "Enable validator incentives", + Changes: []paramsproposal.ParamChange{ + { + Subspace: "incentives", + Key: "ValidatorIncentivesCutoffHeight", + Value: fmt.Sprintf("\"%d\"", cutoffHeight), + }, + { + Subspace: "incentives", + Key: "ValidatorMaxDistributionPerBlock", + Value: fmt.Sprintf("{\"denom\":\"%s\",\"amount\":\"%d\"}", params.BaseCoinUnit, 1000000), + }, + }, + } + + proposalMsg, err := govtypesv1beta1.NewMsgSubmitProposal( + &proposal, + sdk.Coins{ + { + Denom: testDenom, + Amount: stakeAmount.Quo(sdk.NewInt(2)), + }, + }, + proposer.address(), + ) + s.Require().NoError(err) + submitProposalResponse, err := s.chain.sendMsgs(*proposerCtx, proposalMsg) + s.Require().NoError(err) + s.Require().Zero(submitProposalResponse.Code, "raw log: %s", submitProposalResponse.RawLog) + + s.T().Log("Checking proposal was submitted correctly") + govQueryClient := govtypesv1beta1.NewQueryClient(orchClientCtx) + s.Require().Eventually(func() bool { + proposalsQueryResponse, err := govQueryClient.Proposals(context.Background(), &govtypesv1beta1.QueryProposalsRequest{}) + if err != nil { + s.T().Logf("error querying proposals: %e", err) + return false + } + + s.Require().NotEmpty(proposalsQueryResponse.Proposals) + s.Require().Equal(uint64(1), proposalsQueryResponse.Proposals[0].ProposalId, "not proposal id 1") + s.Require().Equal(govtypesv1beta1.StatusVotingPeriod, proposalsQueryResponse.Proposals[0].Status, "proposal not in voting period") + + return true + }, time.Second*30, time.Second*5, "proposal submission was never found") + + s.T().Log("Vote for proposal") + for _, val := range s.chain.validators { + kr, err := val.keyring() + s.Require().NoError(err) + localClientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kr, "val", val.address()) + s.Require().NoError(err) + + voteMsg := govtypesv1beta1.NewMsgVote(val.address(), 1, govtypesv1beta1.OptionYes) + voteResponse, err := s.chain.sendMsgs(*localClientCtx, voteMsg) + s.Require().NoError(err) + s.Require().Zero(voteResponse.Code, "Vote error: %s", voteResponse.RawLog) + } + + // Wait for proposal to be approved + s.T().Log("Waiting for proposal to be approved") + s.Require().Eventually(func() bool { + proposalQueryResponse, _ := govQueryClient.Proposal(context.Background(), &govtypesv1beta1.QueryProposalRequest{ProposalId: 1}) + return govtypesv1beta1.StatusPassed == proposalQueryResponse.Proposal.Status + }, time.Second*30, time.Second*5, "proposal was never accepted") + s.T().Log("proposal approved!") + + // Wait for a few blocks to pass to allow the validator rewards to increase + s.T().Log("Waiting for a few blocks to pass") + s.waitForBlocks(10) + + // Get the updated outstanding rewards for the validator + s.T().Log("Getting updated validator rewards") + currentRewards2, err := s.getValidatorOutstandingRewards(validator) + s.Require().NoError(err) + + // Check if the validator's outstanding rewards have increased + s.T().Logf("Initial rewards: %s, updated rewards: %s", initialRewards.Rewards, currentRewards2.Rewards) + s.Require().True(currentRewards2.Rewards.AmountOf(params.BaseCoinUnit).GT(initialRewards.Rewards.AmountOf(params.BaseCoinUnit)), + "Expected validator rewards to increase, got initial: %s, updated: %s", initialRewards.Rewards, currentRewards2.Rewards) + + s.T().Logf("Validator rewards increased from %s to %s", initialRewards.Rewards, currentRewards2.Rewards) + + s.T().Logf("Waiting to see validator rewards cut off at height %d", cutoffHeight) + s.waitUntilHeight(int64(cutoffHeight)) + + s.T().Log("Getting current validator rewards") + currentRewards, err := s.getValidatorOutstandingRewards(validator) + s.Require().NoError(err) + + s.T().Logf("Current rewards: %s", currentRewards.Rewards) + + s.T().Log("Waiting for a few blocks to pass") + s.waitForBlocks(10) + + s.T().Log("Getting updated validator rewards") + currentRewards2, err = s.getValidatorOutstandingRewards(validator) + s.Require().NoError(err) + + s.T().Logf("Current rewards: %s", currentRewards2.Rewards) + s.Require().Equal(currentRewards.Rewards, currentRewards2.Rewards, "Expected validator rewards to remain constant after cutoff height") + s.T().Log("Validator rewards ended!") + + s.T().Log("Getting sum of all validator rewards") + totalRewards := sdk.DecCoins{} + for _, val := range s.chain.validators { + rewards, err := s.getValidatorOutstandingRewards(val) + s.Require().NoError(err) + totalRewards = totalRewards.Add(rewards.Rewards...) + } + s.T().Logf("Total rewards: %s", totalRewards) + + s.T().Log("Getting community pool balance") + queryRes, err = queryClient.CommunityPool(ctx, &disttypes.QueryCommunityPoolRequest{}) + s.Require().NoError(err) + s.T().Logf("Community pool balance: %s", queryRes.Pool.String()) + + // Subtract the initial rewards and the tx fees from the total rewards to get the incentive rewards + s.T().Log("Total incentive rewards is current rewards minus initial rewards minus tx fees from the proposal submission and votes") + totalIncentiveRewards := totalRewards.Sub(initialRewards.Rewards.MulDec(sdk.NewDec(4))).Sub(sdk.DecCoins{ + { + Denom: testDenom, + Amount: sdk.NewDec(246913560), + }, + }.MulDec(sdk.NewDec(5))) + s.T().Logf("Total incentive rewards: %s", totalIncentiveRewards) + + s.T().Log("Checking that the total incentive rewards are equal to the community pool balance") + s.T().Logf("Initial community pool: %s, updated community pool: %s", initialCommunityPool, queryRes.Pool) + s.Require().Equal(totalIncentiveRewards, initialCommunityPool.Sub(queryRes.Pool), "Expected sum of all validator rewards to be equal to the change in community pool balance") +} + +func (s *IntegrationTestSuite) getValidatorOutstandingRewards(val *validator) (disttypes.ValidatorOutstandingRewards, error) { + ctx := context.Background() + kb, err := val.keyring() + s.Require().NoError(err) + clientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kb, "val", val.address()) + s.Require().NoError(err) + queryClient := disttypes.NewQueryClient(clientCtx) + resp, err := queryClient.ValidatorOutstandingRewards( + ctx, + &disttypes.QueryValidatorOutstandingRewardsRequest{ + ValidatorAddress: val.validatorAddress().String(), + }, + ) + if err != nil { + return disttypes.ValidatorOutstandingRewards{}, err + } + return resp.Rewards, nil +} + +func (s *IntegrationTestSuite) waitForBlocks(numBlocks int64) error { + validator := s.chain.validators[0] + kb, err := validator.keyring() + s.Require().NoError(err) + clientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kb, "val", validator.address()) + s.Require().NoError(err) + + initialHeight, err := s.getCurrentHeight(clientCtx) + s.Require().NoError(err) + targetHeight := initialHeight + numBlocks + + for { + height, err := s.getCurrentHeight(clientCtx) + if err != nil { + return err + } + + if height >= targetHeight { + break + } + + time.Sleep(time.Second) + } + + return nil +} + +func (s *IntegrationTestSuite) waitUntilHeight(height int64) error { + validator := s.chain.validators[0] + kb, err := validator.keyring() + s.Require().NoError(err) + clientCtx, err := s.chain.clientContext("tcp://localhost:26657", &kb, "val", validator.address()) + s.Require().NoError(err) + + errorsTotal := 0 + for { + if errorsTotal > 5 { + return fmt.Errorf("failed to get to height %d: too many errors", height) + } + + currentHeight, err := s.getCurrentHeight(clientCtx) + if err != nil { + errorsTotal++ + continue + } + + if currentHeight >= height { + break + } + + time.Sleep(time.Second * 3) + } + + return nil +} diff --git a/integration_tests/setup_test.go b/integration_tests/setup_test.go index a0321c955..c2b43949c 100644 --- a/integration_tests/setup_test.go +++ b/integration_tests/setup_test.go @@ -534,11 +534,8 @@ func (s *IntegrationTestSuite) initGenesis() { s.Require().NoError(err) appGenState[gravitytypes.ModuleName] = bz - // incentivesGenState := incentivestypes.DefaultGenesisState() - // s.Require().NoError(cdc.UnmarshalJSON(appGenState[gravitytypes.ModuleName], &gravityGenState)) - // bz, err = cdc.MarshalJSON(&incentivesGenState) - // s.Require().NoError(err) - // appGenState[incentivestypes.ModuleName] = bz + // set incentives gen state + s.Require().NoError(s.setIncentivesGenState(appGenState)) // serialize genesis state bz, err = json.MarshalIndent(appGenState, "", " ") diff --git a/proto/incentives/v1/genesis.proto b/proto/incentives/v1/genesis.proto index 66a332344..c422c4787 100644 --- a/proto/incentives/v1/genesis.proto +++ b/proto/incentives/v1/genesis.proto @@ -18,4 +18,12 @@ message Params { // IncentivesCutoffHeight defines the block height after which the incentives module will stop sending coins to the distribution module from // the community pool uint64 incentives_cutoff_height = 2; + // ValidatorMaxDistributionPerBlock defines the maximum coins to be sent directly to voters in the last block from the community pool every block. Leftover coins remain in the community pool. + cosmos.base.v1beta1.Coin validator_max_distribution_per_block = 3 [(gogoproto.nullable) = false]; + // ValidatorIncentivesCutoffHeight defines the block height after which the validator incentives will be stopped + uint64 validator_incentives_cutoff_height = 4; + // ValidatorIncentivesMaxFraction defines the maximum fraction of the validator distribution per block that can be sent to a single validator + string validator_incentives_max_fraction = 5 [(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false]; + // ValidatorIncentivesSetSizeLimit defines the max number of validators to apportion the validator distribution per block to + uint64 validator_incentives_set_size_limit = 6; } diff --git a/x/axelarcork/client/cli/query.go b/x/axelarcork/client/cli/query.go index 94e8781f9..cb0c33fff 100644 --- a/x/axelarcork/client/cli/query.go +++ b/x/axelarcork/client/cli/query.go @@ -297,7 +297,7 @@ func queryScheduledCorksByID() *cobra.Command { func queryCorkResult() *cobra.Command { cmd := &cobra.Command{ - Use: "cork-result [chain-id] [cork-id]", + Use: "cork-result [cork-id] [chain-id]", Aliases: []string{"cr"}, Args: cobra.ExactArgs(2), Short: "query cork result from the chain", diff --git a/x/cellarfees/migrations/v1/keeper/query_server.go b/x/cellarfees/migrations/v1/keeper/query_server.go index ac9a70663..569976445 100644 --- a/x/cellarfees/migrations/v1/keeper/query_server.go +++ b/x/cellarfees/migrations/v1/keeper/query_server.go @@ -5,51 +5,50 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/peggyjv/sommelier/v7/x/cellarfees/migrations/v1/types" - v1types "github.com/peggyjv/sommelier/v7/x/cellarfees/migrations/v1/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) var _ types.QueryServer = Keeper{} -func (k Keeper) QueryParams(c context.Context, req *v1types.QueryParamsRequest) (*v1types.QueryParamsResponse, error) { +func (k Keeper) QueryParams(c context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "empty request") } - return &v1types.QueryParamsResponse{ + return &types.QueryParamsResponse{ Params: k.GetParams(sdk.UnwrapSDKContext(c)), }, nil } -func (k Keeper) QueryModuleAccounts(c context.Context, req *v1types.QueryModuleAccountsRequest) (*v1types.QueryModuleAccountsResponse, error) { +func (k Keeper) QueryModuleAccounts(c context.Context, req *types.QueryModuleAccountsRequest) (*types.QueryModuleAccountsResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "empty request") } - return &v1types.QueryModuleAccountsResponse{ + return &types.QueryModuleAccountsResponse{ FeesAddress: k.GetFeesAccount(sdk.UnwrapSDKContext(c)).GetAddress().String(), }, nil } -func (k Keeper) QueryLastRewardSupplyPeak(c context.Context, req *v1types.QueryLastRewardSupplyPeakRequest) (*v1types.QueryLastRewardSupplyPeakResponse, error) { +func (k Keeper) QueryLastRewardSupplyPeak(c context.Context, req *types.QueryLastRewardSupplyPeakRequest) (*types.QueryLastRewardSupplyPeakResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "empty request") } - return &v1types.QueryLastRewardSupplyPeakResponse{LastRewardSupplyPeak: k.GetLastRewardSupplyPeak(sdk.UnwrapSDKContext(c))}, nil + return &types.QueryLastRewardSupplyPeakResponse{LastRewardSupplyPeak: k.GetLastRewardSupplyPeak(sdk.UnwrapSDKContext(c))}, nil } -func (k Keeper) QueryFeeAccrualCounters(c context.Context, req *v1types.QueryFeeAccrualCountersRequest) (*v1types.QueryFeeAccrualCountersResponse, error) { +func (k Keeper) QueryFeeAccrualCounters(c context.Context, req *types.QueryFeeAccrualCountersRequest) (*types.QueryFeeAccrualCountersResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "empty request") } - return &v1types.QueryFeeAccrualCountersResponse{FeeAccrualCounters: k.GetFeeAccrualCounters(sdk.UnwrapSDKContext(c))}, nil + return &types.QueryFeeAccrualCountersResponse{FeeAccrualCounters: k.GetFeeAccrualCounters(sdk.UnwrapSDKContext(c))}, nil } -func (k Keeper) QueryAPY(c context.Context, _ *v1types.QueryAPYRequest) (*v1types.QueryAPYResponse, error) { - return &v1types.QueryAPYResponse{ +func (k Keeper) QueryAPY(c context.Context, _ *types.QueryAPYRequest) (*types.QueryAPYResponse, error) { + return &types.QueryAPYResponse{ Apy: k.GetAPY(sdk.UnwrapSDKContext(c)).String(), }, nil } diff --git a/x/cellarfees/migrations/v1/types/errors.go b/x/cellarfees/migrations/v1/types/errors.go index 8e55258b6..a2fa9199e 100644 --- a/x/cellarfees/migrations/v1/types/errors.go +++ b/x/cellarfees/migrations/v1/types/errors.go @@ -7,10 +7,5 @@ import ( // x/cellarfees module sentinel errors var ( ErrInvalidFeeAccrualAuctionThreshold = errorsmod.Register(ModuleName, 2, "invalid fee accrual auction threshold") - ErrInvalidRewardEmissionPeriod = errorsmod.Register(ModuleName, 3, "invalid reward emission period") - ErrInvalidInitialPriceDecreaseRate = errorsmod.Register(ModuleName, 4, "invalid initial price decrease rate") - ErrInvalidPriceDecreaseBlockInterval = errorsmod.Register(ModuleName, 5, "invalid price decrease block interval") ErrInvalidFeeAccrualCounters = errorsmod.Register(ModuleName, 6, "invalid fee accrual counters") - ErrInvalidLastRewardSupplyPeak = errorsmod.Register(ModuleName, 7, "invalid last reward supply peak") - ErrInvalidAuctionInterval = errorsmod.Register(ModuleName, 8, "invalid interval blocks between auctions") ) diff --git a/x/cellarfees/migrations/v1/types/genesis.go b/x/cellarfees/migrations/v1/types/genesis.go index 774a3274f..2fae372c2 100644 --- a/x/cellarfees/migrations/v1/types/genesis.go +++ b/x/cellarfees/migrations/v1/types/genesis.go @@ -4,6 +4,7 @@ import ( "sort" sdk "github.com/cosmos/cosmos-sdk/types" + types "github.com/peggyjv/sommelier/v7/x/cellarfees/types" ) const DefaultParamspace = ModuleName @@ -37,7 +38,7 @@ func (gs GenesisState) Validate() error { } if gs.LastRewardSupplyPeak.LT(sdk.ZeroInt()) { - return ErrInvalidLastRewardSupplyPeak.Wrap("last reward supply peak cannot be less than zero!") + return types.ErrInvalidLastRewardSupplyPeak.Wrap("last reward supply peak cannot be less than zero!") } return nil diff --git a/x/cellarfees/migrations/v1/types/params.go b/x/cellarfees/migrations/v1/types/params.go index 0d81222a0..a1a7b6747 100644 --- a/x/cellarfees/migrations/v1/types/params.go +++ b/x/cellarfees/migrations/v1/types/params.go @@ -5,6 +5,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "gopkg.in/yaml.v2" + + types "github.com/peggyjv/sommelier/v7/x/cellarfees/types" ) const ( @@ -92,11 +94,11 @@ func validateFeeAccrualAuctionThreshold(i interface{}) error { func validateRewardEmissionPeriod(i interface{}) error { emissionPeriod, ok := i.(uint64) if !ok { - return errorsmod.Wrapf(ErrInvalidRewardEmissionPeriod, "reward emission period: %T", i) + return errorsmod.Wrapf(types.ErrInvalidRewardEmissionPeriod, "reward emission period: %T", i) } if emissionPeriod == 0 { - return errorsmod.Wrapf(ErrInvalidRewardEmissionPeriod, "reward emission period cannot be zero") + return errorsmod.Wrapf(types.ErrInvalidRewardEmissionPeriod, "reward emission period cannot be zero") } return nil @@ -105,15 +107,15 @@ func validateRewardEmissionPeriod(i interface{}) error { func validateInitialPriceDecreaseRate(i interface{}) error { rate, ok := i.(sdk.Dec) if !ok { - return errorsmod.Wrapf(ErrInvalidInitialPriceDecreaseRate, "initial price decrease rate: %T", i) + return errorsmod.Wrapf(types.ErrInvalidInitialPriceDecreaseRate, "initial price decrease rate: %T", i) } if rate == sdk.ZeroDec() { - return errorsmod.Wrapf(ErrInvalidInitialPriceDecreaseRate, "initial price decrease rate cannot be zero, must be 0 < x < 1") + return errorsmod.Wrapf(types.ErrInvalidInitialPriceDecreaseRate, "initial price decrease rate cannot be zero, must be 0 < x < 1") } if rate == sdk.OneDec() { - return errorsmod.Wrapf(ErrInvalidInitialPriceDecreaseRate, "initial price decrease rate cannot be one, must be 0 < x < 1") + return errorsmod.Wrapf(types.ErrInvalidInitialPriceDecreaseRate, "initial price decrease rate cannot be one, must be 0 < x < 1") } return nil @@ -122,11 +124,11 @@ func validateInitialPriceDecreaseRate(i interface{}) error { func validatePriceDecreaseBlockInterval(i interface{}) error { interval, ok := i.(uint64) if !ok { - return errorsmod.Wrapf(ErrInvalidPriceDecreaseBlockInterval, "price decrease block interval: %T", i) + return errorsmod.Wrapf(types.ErrInvalidPriceDecreaseBlockInterval, "price decrease block interval: %T", i) } if interval == 0 { - return errorsmod.Wrapf(ErrInvalidPriceDecreaseBlockInterval, "price decrease block interval cannot be zero") + return errorsmod.Wrapf(types.ErrInvalidPriceDecreaseBlockInterval, "price decrease block interval cannot be zero") } return nil @@ -135,11 +137,11 @@ func validatePriceDecreaseBlockInterval(i interface{}) error { func validateAuctionInterval(i interface{}) error { interval, ok := i.(uint64) if !ok { - return errorsmod.Wrapf(ErrInvalidAuctionInterval, "auction interval: %T", i) + return errorsmod.Wrapf(types.ErrInvalidAuctionInterval, "auction interval: %T", i) } if interval == 0 { - return errorsmod.Wrapf(ErrInvalidAuctionInterval, "auction interval cannot be zero") + return errorsmod.Wrapf(types.ErrInvalidAuctionInterval, "auction interval cannot be zero") } return nil diff --git a/x/cellarfees/types/errors.go b/x/cellarfees/types/errors.go index 8e587e9b1..2a1910f0d 100644 --- a/x/cellarfees/types/errors.go +++ b/x/cellarfees/types/errors.go @@ -6,11 +6,11 @@ import ( // x/cellarfees module sentinel errors var ( - ErrInvalidFeeAccrualAuctionThreshold = errorsmod.Register(ModuleName, 2, "invalid fee accrual auction threshold") + // Codes 2 and 6 were deleted during v2 module upgrade + ErrInvalidRewardEmissionPeriod = errorsmod.Register(ModuleName, 3, "invalid reward emission period") ErrInvalidInitialPriceDecreaseRate = errorsmod.Register(ModuleName, 4, "invalid initial price decrease rate") ErrInvalidPriceDecreaseBlockInterval = errorsmod.Register(ModuleName, 5, "invalid price decrease block interval") - ErrInvalidFeeAccrualCounters = errorsmod.Register(ModuleName, 6, "invalid fee accrual counters") ErrInvalidLastRewardSupplyPeak = errorsmod.Register(ModuleName, 7, "invalid last reward supply peak") ErrInvalidAuctionInterval = errorsmod.Register(ModuleName, 8, "invalid interval blocks between auctions") ErrInvalidAuctionThresholdUsdValue = errorsmod.Register(ModuleName, 9, "invalid auction threshold USD value") diff --git a/x/cork/client/cli/query.go b/x/cork/client/cli/query.go index 34b8bdaed..8710f5f97 100644 --- a/x/cork/client/cli/query.go +++ b/x/cork/client/cli/query.go @@ -227,7 +227,7 @@ func queryScheduledCorksByID() *cobra.Command { func queryCorkResult() *cobra.Command { cmd := &cobra.Command{ - Use: "cork-result", + Use: "cork-result [cork-id]", Aliases: []string{"cr"}, Args: cobra.ExactArgs(1), Short: "query cork result from the chain", diff --git a/x/incentives/keeper/abci.go b/x/incentives/keeper/abci.go index 345259bc2..2290da606 100644 --- a/x/incentives/keeper/abci.go +++ b/x/incentives/keeper/abci.go @@ -1,13 +1,48 @@ package keeper import ( + abci "github.com/cometbft/cometbft/abci/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" sdk "github.com/cosmos/cosmos-sdk/types" ) -func (k Keeper) BeginBlocker(ctx sdk.Context) {} +// BeginBlocker defines distribution rewards for validators +// +// 1) Subtract the total distribution from the community pool +// 2) Get a list of qualifying validators sorted by descending power +// 3) Allocate tokens to qualifying validators proportionally to their power with a cap +// 4) Add the remaining coins back to the community pool +func (k Keeper) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) { + incentivesParams := k.GetParamSet(ctx) + if uint64(ctx.BlockHeight()) >= incentivesParams.ValidatorIncentivesCutoffHeight || incentivesParams.ValidatorMaxDistributionPerBlock.IsZero() { + return + } + + // Rewards come from the community pool + totalDistribution := sdk.NewDecCoinsFromCoins(incentivesParams.ValidatorMaxDistributionPerBlock) + feePool := k.DistributionKeeper.GetFeePool(ctx) + newPool, negative := feePool.CommunityPool.SafeSub(totalDistribution) + if negative { + k.Logger(ctx).Error("Insufficient coins in community to distribute to validators", "community pool", feePool.CommunityPool) + return + } + + // Get a list of qualifying validators sorted by descending power + valInfos := k.getValidatorInfos(ctx, req) + sortedValInfos := sortValidatorInfosByPower(valInfos) + qualifyingVoters := truncateVoters(sortedValInfos, incentivesParams.ValidatorIncentivesSetSizeLimit) + + // Allocate tokens to qualifying validators proportionally to their power with a cap + totalPower := getTotalPower(&qualifyingVoters) + remaining := k.AllocateTokens(ctx, totalPower, totalDistribution, qualifyingVoters, incentivesParams.ValidatorIncentivesMaxFraction) + + // Add the remaining coins back to the community pool + newPool = newPool.Add(remaining...) + feePool.CommunityPool = newPool + k.DistributionKeeper.SetFeePool(ctx, feePool) +} // EndBlocker defines Distribution of incentives to stakers // diff --git a/x/incentives/keeper/abci_test.go b/x/incentives/keeper/abci_test.go index 39d1745f3..0ecbbb64c 100644 --- a/x/incentives/keeper/abci_test.go +++ b/x/incentives/keeper/abci_test.go @@ -1,8 +1,10 @@ package keeper import ( + abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" distributionTypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/golang/mock/gomock" "github.com/peggyjv/sommelier/v7/app/params" incentivesTypes "github.com/peggyjv/sommelier/v7/x/incentives/types" ) @@ -17,20 +19,20 @@ func (suite *KeeperTestSuite) TestEndBlockerIncentivesDisabledDoesNothing() { // By not mocking any other calls, the test will panic and fail if an unmocked keeper function is called, // implying that the function isn't exiting early as designed. - require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx) }) + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) require.NotPanics(func() { incentivesKeeper.EndBlocker(ctx) }) incentivesParams.DistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.OneInt()) incentivesKeeper.SetParams(ctx, incentivesParams) - require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx) }) + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) require.NotPanics(func() { incentivesKeeper.EndBlocker(ctx) }) incentivesParams.DistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.ZeroInt()) incentivesParams.IncentivesCutoffHeight = 1500 incentivesKeeper.SetParams(ctx, incentivesParams) - require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx) }) + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) require.NotPanics(func() { incentivesKeeper.EndBlocker(ctx) }) } @@ -53,6 +55,112 @@ func (suite *KeeperTestSuite) TestEndBlockerInsufficientCommunityPoolBalance() { // By not mocking the bank SendModuleToModule call, the test will panic and fail if the community pool balance // check branch isn't taken as intended. - require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx) }) + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) require.NotPanics(func() { incentivesKeeper.EndBlocker(ctx) }) } + +func (suite *KeeperTestSuite) TestBeginBlockerIncentivesDisabled() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + require := suite.Require() + + incentivesParams := incentivesTypes.DefaultParams() + incentivesParams.ValidatorIncentivesCutoffHeight = 100 + incentivesParams.ValidatorMaxDistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.NewInt(1000)) + incentivesKeeper.SetParams(ctx, incentivesParams) + + // Set block height above cutoff + ctx = ctx.WithBlockHeight(101) + + // By not mocking any other calls, the test will panic and fail if an unmocked keeper function is called, + // implying that the function isn't exiting early as designed. + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) + + incentivesParams.ValidatorIncentivesCutoffHeight = 200 + incentivesParams.ValidatorMaxDistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.ZeroInt()) + incentivesKeeper.SetParams(ctx, incentivesParams) + + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) +} + +func (suite *KeeperTestSuite) TestBeginBlockerInsufficientCommunityPoolBalance() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + require := suite.Require() + + incentivesParams := incentivesTypes.DefaultParams() + incentivesParams.ValidatorIncentivesCutoffHeight = 100 + incentivesParams.ValidatorMaxDistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.NewInt(1000)) + incentivesKeeper.SetParams(ctx, incentivesParams) + + // Set block height below cutoff + ctx = ctx.WithBlockHeight(99) + + // Mock insufficient community pool balance + pool := distributionTypes.FeePool{ + CommunityPool: sdk.NewDecCoins(sdk.NewDecCoin(params.BaseCoinUnit, sdk.NewInt(999))), + } + suite.distributionKeeper.EXPECT().GetFeePool(ctx).Return(pool) + + // By not mocking any further calls, the test will panic and fail if the community pool balance + // check branch isn't taken as intended. + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, abci.RequestBeginBlock{}) }) +} + +func (suite *KeeperTestSuite) TestBeginBlockerSuccess() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + require := suite.Require() + + incentivesParams := incentivesTypes.DefaultParams() + incentivesParams.ValidatorIncentivesCutoffHeight = 100 + incentivesParams.ValidatorMaxDistributionPerBlock = sdk.NewCoin(params.BaseCoinUnit, sdk.NewInt(1000)) + incentivesKeeper.SetParams(ctx, incentivesParams) + + // Set block height below cutoff + ctx = ctx.WithBlockHeight(99) + + // Mock sufficient community pool balance + pool := distributionTypes.FeePool{ + CommunityPool: sdk.NewDecCoins(sdk.NewDecCoin(params.BaseCoinUnit, sdk.NewInt(2000))), + } + suite.distributionKeeper.EXPECT().GetFeePool(ctx).Return(pool) + + // Mock validators + validators := suite.getMockValidators() + validator1, validator2 := validators[0], validators[1] + + consAddr1, err := validator1.GetConsAddr() + require.NoError(err) + consAddr2, err := validator2.GetConsAddr() + require.NoError(err) + + // Mock RequestBeginBlock + req := abci.RequestBeginBlock{ + LastCommitInfo: abci.CommitInfo{ + Votes: []abci.VoteInfo{ + {Validator: abci.Validator{Address: consAddr1, Power: 10}, SignedLastBlock: true}, + {Validator: abci.Validator{Address: consAddr2, Power: 20}, SignedLastBlock: true}, + }, + }, + } + + // Mock StakingKeeper expectations + suite.stakingKeeper.EXPECT().ValidatorByConsAddr(ctx, consAddr1).Return(validator1) + suite.stakingKeeper.EXPECT().ValidatorByConsAddr(ctx, consAddr2).Return(validator2) + + // Mock DistributionKeeper expectations for AllocateTokens + suite.distributionKeeper.EXPECT().GetValidatorCurrentRewards(ctx, validator1.GetOperator()).Return(distributionTypes.ValidatorCurrentRewards{Rewards: sdk.DecCoins{}}) + suite.distributionKeeper.EXPECT().SetValidatorCurrentRewards(ctx, validator1.GetOperator(), gomock.Any()) + suite.distributionKeeper.EXPECT().GetValidatorOutstandingRewards(ctx, validator1.GetOperator()).Return(distributionTypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) + suite.distributionKeeper.EXPECT().SetValidatorOutstandingRewards(ctx, validator1.GetOperator(), gomock.Any()) + + suite.distributionKeeper.EXPECT().GetValidatorCurrentRewards(ctx, validator2.GetOperator()).Return(distributionTypes.ValidatorCurrentRewards{Rewards: sdk.DecCoins{}}) + suite.distributionKeeper.EXPECT().SetValidatorCurrentRewards(ctx, validator2.GetOperator(), gomock.Any()) + suite.distributionKeeper.EXPECT().GetValidatorOutstandingRewards(ctx, validator2.GetOperator()).Return(distributionTypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) + suite.distributionKeeper.EXPECT().SetValidatorOutstandingRewards(ctx, validator2.GetOperator(), gomock.Any()) + + // Mock setting the updated fee pool + suite.distributionKeeper.EXPECT().SetFeePool(ctx, gomock.Any()) + + require.NotPanics(func() { incentivesKeeper.BeginBlocker(ctx, req) }) + + // You can add more specific assertions here if needed, such as checking emitted events +} diff --git a/x/incentives/keeper/incentives.go b/x/incentives/keeper/incentives.go new file mode 100644 index 000000000..f8d01f355 --- /dev/null +++ b/x/incentives/keeper/incentives.go @@ -0,0 +1,117 @@ +package keeper + +import ( + "sort" + + "cosmossdk.io/math" + + abci "github.com/cometbft/cometbft/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/peggyjv/sommelier/v7/x/incentives/types" +) + +type ValidatorInfo struct { + Validator stakingtypes.ValidatorI + Power int64 +} + +// sortValidatorInfosByPower sorts the validator information by power in descending order +func sortValidatorInfosByPower(valInfos []ValidatorInfo) []ValidatorInfo { + sort.Slice(valInfos, func(i, j int) bool { + return valInfos[i].Power > valInfos[j].Power + }) + + return valInfos +} + +// GetTotalPower returns the total power of the passed in validatorInfos +func getTotalPower(valInfos *[]ValidatorInfo) int64 { + totalPower := int64(0) + for _, valInfo := range *valInfos { + totalPower += valInfo.Power + } + + return totalPower +} + +// getValidatorInfos returns the validator information for the voters in the last block +func (k Keeper) getValidatorInfos(ctx sdk.Context, req abci.RequestBeginBlock) []ValidatorInfo { + validatorInfos := []ValidatorInfo{} + for _, vote := range req.LastCommitInfo.GetVotes() { + if !vote.SignedLastBlock { + continue + } + + validator := k.StakingKeeper.ValidatorByConsAddr(ctx, vote.Validator.Address) + validatorInfos = append(validatorInfos, ValidatorInfo{ + Validator: validator, + Power: vote.Validator.Power, + }) + } + return validatorInfos +} + +// truncateVoters returns the first maxSize validatorInfos +func truncateVoters(validatorInfos []ValidatorInfo, maxSize uint64) []ValidatorInfo { + if len(validatorInfos) > int(maxSize) { + return validatorInfos[:maxSize] + } + + return validatorInfos +} + +// AllocateTokens performs reward distribution to the provided validators proportionally to their power with a cap +func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64, totalDistribution sdk.DecCoins, qualifyingVoters []ValidatorInfo, maxFraction sdk.Dec) sdk.DecCoins { + remaining := totalDistribution + + for _, valInfo := range qualifyingVoters { + validator := valInfo.Validator + powerFraction := math.LegacyNewDec(valInfo.Power).QuoInt64(totalPreviousPower) + + // Cap at the max fraction + if powerFraction.GT(maxFraction) { + powerFraction = maxFraction + } + + reward := totalDistribution.MulDecTruncate(powerFraction) + + k.AllocateTokensToValidator(ctx, validator, reward) + remaining = remaining.Sub(reward) + } + + // Only emit the total distribution reward event if there are qualifying voters + if len(qualifyingVoters) > 0 { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeTotalValidatorIncentivesRewards, + sdk.NewAttribute(sdk.AttributeKeyAmount, totalDistribution.Sub(remaining).String()), + ), + ) + } + + return remaining +} + +// AllocateTokensToValidator allocates tokens to a particular validator. +// All tokens go to the validator. +func (k Keeper) AllocateTokensToValidator(ctx sdk.Context, val stakingtypes.ValidatorI, tokens sdk.DecCoins) { + // Update validator rewards + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeValidatorIncentivesReward, + sdk.NewAttribute(sdk.AttributeKeyAmount, tokens.String()), + sdk.NewAttribute(types.AttributeKeyValidator, val.GetOperator().String()), + ), + ) + + // Update current rewards + currentRewards := k.DistributionKeeper.GetValidatorCurrentRewards(ctx, val.GetOperator()) + currentRewards.Rewards = currentRewards.Rewards.Add(tokens...) + k.DistributionKeeper.SetValidatorCurrentRewards(ctx, val.GetOperator(), currentRewards) + + // Update outstanding rewards + outstanding := k.DistributionKeeper.GetValidatorOutstandingRewards(ctx, val.GetOperator()) + outstanding.Rewards = outstanding.Rewards.Add(tokens...) + k.DistributionKeeper.SetValidatorOutstandingRewards(ctx, val.GetOperator(), outstanding) +} diff --git a/x/incentives/keeper/incentives_test.go b/x/incentives/keeper/incentives_test.go new file mode 100644 index 000000000..480c0bbf5 --- /dev/null +++ b/x/incentives/keeper/incentives_test.go @@ -0,0 +1,366 @@ +package keeper + +import ( + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + ccrypto "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/peggyjv/sommelier/v7/x/incentives/types" +) + +var ( + // ConsPrivKeys generate ed25519 ConsPrivKeys to be used for validator operator keys + ConsPrivKeys = []ccrypto.PrivKey{ + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + } + + // ConsPubKeys holds the consensus public keys to be used for validator operator keys + ConsPubKeys = []ccrypto.PubKey{ + ConsPrivKeys[0].PubKey(), + ConsPrivKeys[1].PubKey(), + ConsPrivKeys[2].PubKey(), + ConsPrivKeys[3].PubKey(), + ConsPrivKeys[4].PubKey(), + } +) + +func (suite *KeeperTestSuite) getMockValidators() []*stakingtypes.Validator { + validator1, err := stakingtypes.NewValidator(sdk.ValAddress([]byte("val1val1val1val1val1")), ConsPubKeys[0], stakingtypes.Description{}) + suite.Require().NoError(err) + validator2, err := stakingtypes.NewValidator(sdk.ValAddress([]byte("val2val2val2val2val2")), ConsPubKeys[1], stakingtypes.Description{}) + suite.Require().NoError(err) + validator3, err := stakingtypes.NewValidator(sdk.ValAddress([]byte("val3val3val3val3val3")), ConsPubKeys[2], stakingtypes.Description{}) + suite.Require().NoError(err) + return []*stakingtypes.Validator{&validator1, &validator2, &validator3} +} + +func (suite *KeeperTestSuite) TestGetValidatorInfos() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + + // Create mock validators + validators := suite.getMockValidators() + validator1, validator2, validator3 := validators[0], validators[1], validators[2] + + consAddr1, err := validator1.GetConsAddr() + suite.Require().NoError(err) + consAddr2, err := validator2.GetConsAddr() + suite.Require().NoError(err) + consAddr3, err := validator3.GetConsAddr() + suite.Require().NoError(err) + + // Create mock RequestBeginBlock + req := abci.RequestBeginBlock{ + LastCommitInfo: abci.CommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: abci.Validator{Address: consAddr1, Power: 10}, + SignedLastBlock: true, + }, + { + Validator: abci.Validator{Address: consAddr2, Power: 20}, + SignedLastBlock: true, + }, + { + Validator: abci.Validator{Address: consAddr3, Power: 30}, + SignedLastBlock: false, + }, + }, + }, + } + + // Set up expectations for the mock StakingKeeper + suite.stakingKeeper.EXPECT().ValidatorByConsAddr(ctx, consAddr1).Return(validator1) + suite.stakingKeeper.EXPECT().ValidatorByConsAddr(ctx, consAddr2).Return(validator2) + + // Call the function being tested + validatorInfos := incentivesKeeper.getValidatorInfos(ctx, req) + + // Assert the results + suite.Require().Len(validatorInfos, 2) + suite.Require().Equal(validator1, validatorInfos[0].Validator) + suite.Require().Equal(int64(10), validatorInfos[0].Power) + suite.Require().Equal(validator2, validatorInfos[1].Validator) + suite.Require().Equal(int64(20), validatorInfos[1].Power) +} + +func (suite *KeeperTestSuite) TestGetValidatorInfosNoSigners() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + + // Create mock RequestBeginBlock with no signers + req := abci.RequestBeginBlock{ + LastCommitInfo: abci.CommitInfo{ + Votes: []abci.VoteInfo{ + { + Validator: abci.Validator{Address: []byte("val1"), Power: 10}, + SignedLastBlock: false, + }, + { + Validator: abci.Validator{Address: []byte("val2"), Power: 20}, + SignedLastBlock: false, + }, + }, + }, + } + + // Call the function being tested + validatorInfos := incentivesKeeper.getValidatorInfos(ctx, req) + + // Assert the results + suite.Require().Len(validatorInfos, 0) +} + +func (suite *KeeperTestSuite) TestSortValidatorInfosByPower() { + // Create a slice of ValidatorInfo with unsorted power + valInfos := []ValidatorInfo{ + {Power: 30}, + {Power: 10}, + {Power: 50}, + {Power: 20}, + {Power: 40}, + } + + // Sort the validator infos + sortedValInfos := sortValidatorInfosByPower(valInfos) + + // Assert the results + suite.Require().Len(sortedValInfos, 5) + suite.Require().Equal(int64(50), sortedValInfos[0].Power) + suite.Require().Equal(int64(40), sortedValInfos[1].Power) + suite.Require().Equal(int64(30), sortedValInfos[2].Power) + suite.Require().Equal(int64(20), sortedValInfos[3].Power) + suite.Require().Equal(int64(10), sortedValInfos[4].Power) +} + +func (suite *KeeperTestSuite) TestTruncateVoters() { + // Create a slice of ValidatorInfo + valInfos := []ValidatorInfo{ + {Power: 30}, + {Power: 10}, + {Power: 50}, + {Power: 20}, + {Power: 40}, + } + + // Get the truncated voters + truncatedVoters := truncateVoters(valInfos, 3) + + // Assert the results + suite.Require().Len(truncatedVoters, 3) + suite.Require().Equal(int64(30), truncatedVoters[0].Power) + suite.Require().Equal(int64(10), truncatedVoters[1].Power) + suite.Require().Equal(int64(50), truncatedVoters[2].Power) +} + +func (suite *KeeperTestSuite) TestSortValidatorInfosByPowerEmptySlice() { + // Create an empty slice of ValidatorInfo + var valInfos []ValidatorInfo + + // Sort the validator infos + sortedValInfos := sortValidatorInfosByPower(valInfos) + + // Assert the results + suite.Require().Len(sortedValInfos, 0) +} + +func (suite *KeeperTestSuite) TestGetTotalPower() { + // Create a slice of ValidatorInfo + valInfos := []ValidatorInfo{ + {Power: 30}, + {Power: 10}, + {Power: 50}, + {Power: 20}, + {Power: 40}, + } + + // Get the total power + totalPower := getTotalPower(&valInfos) + + // Assert the result + suite.Require().Equal(int64(150), totalPower) +} + +func (suite *KeeperTestSuite) TestGetTotalPowerEmptySlice() { + // Create an empty slice of ValidatorInfo + var valInfos []ValidatorInfo + + // Get the total power + totalPower := getTotalPower(&valInfos) + + // Assert the result + suite.Require().Equal(int64(0), totalPower) +} + +func (suite *KeeperTestSuite) TestAllocateTokensToValidator() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + + // Create a mock validator + valAddr := sdk.ValAddress([]byte("validatorvalidatorva")) + validator, err := stakingtypes.NewValidator(valAddr, ConsPubKeys[0], stakingtypes.Description{}) + suite.Require().NoError(err) + + // Create mock tokens to allocate + tokens := sdk.NewDecCoins(sdk.NewDecCoin("usom", sdk.NewInt(100))) + + // Set up expectations for the mock DistributionKeeper + currentRewards := distributiontypes.ValidatorCurrentRewards{Rewards: sdk.DecCoins{}} + outstandingRewards := distributiontypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}} + + suite.distributionKeeper.EXPECT(). + GetValidatorCurrentRewards(ctx, valAddr). + Return(currentRewards) + suite.distributionKeeper.EXPECT(). + SetValidatorCurrentRewards(ctx, valAddr, distributiontypes.ValidatorCurrentRewards{Rewards: tokens}) + suite.distributionKeeper.EXPECT(). + GetValidatorOutstandingRewards(ctx, valAddr). + Return(outstandingRewards) + suite.distributionKeeper.EXPECT(). + SetValidatorOutstandingRewards(ctx, valAddr, distributiontypes.ValidatorOutstandingRewards{Rewards: tokens}) + + // Call the function being tested + incentivesKeeper.AllocateTokensToValidator(ctx, validator, tokens) + + // Verify that the event was emitted + events := ctx.EventManager().Events() + suite.Require().Len(events, 1) + event := events[0] + suite.Require().Equal(types.EventTypeValidatorIncentivesReward, event.Type) + suite.Require().Len(event.Attributes, 2) + suite.Require().Equal(sdk.AttributeKeyAmount, event.Attributes[0].Key) + suite.Require().Equal(tokens.String(), event.Attributes[0].Value) + suite.Require().Equal(types.AttributeKeyValidator, event.Attributes[1].Key) + suite.Require().Equal(valAddr.String(), event.Attributes[1].Value) +} + +func (suite *KeeperTestSuite) TestAllocateTokensToValidatorWithExistingRewards() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + + // Create a mock validator + valAddr := sdk.ValAddress([]byte("validatorvalidatorva")) + validator, err := stakingtypes.NewValidator(valAddr, ConsPubKeys[0], stakingtypes.Description{}) + suite.Require().NoError(err) + + // Create mock tokens to allocate + existingRewards := sdk.NewDecCoins(sdk.NewDecCoin("usom", sdk.NewInt(50))) + newTokens := sdk.NewDecCoins(sdk.NewDecCoin("usom", sdk.NewInt(100))) + expectedTotalRewards := existingRewards.Add(newTokens...) + + // Set up expectations for the mock DistributionKeeper + currentRewards := distributiontypes.ValidatorCurrentRewards{Rewards: existingRewards} + outstandingRewards := distributiontypes.ValidatorOutstandingRewards{Rewards: existingRewards} + + suite.distributionKeeper.EXPECT(). + GetValidatorCurrentRewards(ctx, valAddr). + Return(currentRewards) + suite.distributionKeeper.EXPECT(). + SetValidatorCurrentRewards(ctx, valAddr, distributiontypes.ValidatorCurrentRewards{Rewards: expectedTotalRewards}) + suite.distributionKeeper.EXPECT(). + GetValidatorOutstandingRewards(ctx, valAddr). + Return(outstandingRewards) + suite.distributionKeeper.EXPECT(). + SetValidatorOutstandingRewards(ctx, valAddr, distributiontypes.ValidatorOutstandingRewards{Rewards: expectedTotalRewards}) + + // Call the function being tested + incentivesKeeper.AllocateTokensToValidator(ctx, validator, newTokens) + + // Verify that the event was emitted + events := ctx.EventManager().Events() + suite.Require().Len(events, 1) + event := events[0] + suite.Require().Equal(types.EventTypeValidatorIncentivesReward, event.Type) + suite.Require().Len(event.Attributes, 2) + suite.Require().Equal(sdk.AttributeKeyAmount, event.Attributes[0].Key) + suite.Require().Equal(newTokens.String(), event.Attributes[0].Value) + suite.Require().Equal(types.AttributeKeyValidator, event.Attributes[1].Key) + suite.Require().Equal(valAddr.String(), event.Attributes[1].Value) +} + +func (suite *KeeperTestSuite) TestAllocateTokens() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + + // Create mock validators + validators := suite.getMockValidators() + validator1, validator2, validator3 := validators[0], validators[1], validators[2] + + // Set up qualifying voters + qualifyingVoters := []ValidatorInfo{ + {Validator: validator1, Power: 30}, + {Validator: validator2, Power: 20}, + {Validator: validator3, Power: 10}, + } + + totalPreviousPower := int64(60) + totalDistribution := sdk.NewDecCoins(sdk.NewDecCoin("usom", sdk.NewInt(100))) + maxFraction := sdk.NewDecWithPrec(5, 1) // 0.5 + + // Set up expectations for the mock DistributionKeeper + totalExpectedRewards := sdk.NewDecCoins() + for _, voter := range qualifyingVoters { + powerFraction := sdk.NewDecFromInt(sdk.NewInt(voter.Power)).QuoInt64(totalPreviousPower) + expectedReward := totalDistribution.MulDecTruncate(powerFraction) + if powerFraction.GT(maxFraction) { + expectedReward = totalDistribution.MulDecTruncate(maxFraction) + } + + totalExpectedRewards = totalExpectedRewards.Add(expectedReward...) + + suite.distributionKeeper.EXPECT(). + GetValidatorCurrentRewards(ctx, voter.Validator.GetOperator()). + Return(distributiontypes.ValidatorCurrentRewards{Rewards: sdk.DecCoins{}}) + suite.distributionKeeper.EXPECT(). + SetValidatorCurrentRewards(ctx, voter.Validator.GetOperator(), distributiontypes.ValidatorCurrentRewards{Rewards: expectedReward}) + suite.distributionKeeper.EXPECT(). + GetValidatorOutstandingRewards(ctx, voter.Validator.GetOperator()). + Return(distributiontypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}}) + suite.distributionKeeper.EXPECT(). + SetValidatorOutstandingRewards(ctx, voter.Validator.GetOperator(), distributiontypes.ValidatorOutstandingRewards{Rewards: expectedReward}) + } + + // Call the function being tested + remaining := incentivesKeeper.AllocateTokens(ctx, totalPreviousPower, totalDistribution, qualifyingVoters, maxFraction) + + // Verify that the sum of remaining and distributed rewards equals totalDistribution + totalAllocated := remaining.Add(totalExpectedRewards...) + suite.Require().Equal(totalDistribution, totalAllocated, "Sum of remaining and distributed rewards should equal total distribution") + + // Verify that events were emitted + totalEvents := len(qualifyingVoters) + 1 // One event for each validator plus one for the total distribution + events := ctx.EventManager().Events() + suite.Require().Len(events, totalEvents) + for i, event := range events { + if i == totalEvents-1 { + suite.Require().Equal(types.EventTypeTotalValidatorIncentivesRewards, event.Type) + suite.Require().Equal(totalDistribution.Sub(remaining).String(), event.Attributes[0].Value) + continue + } else { + suite.Require().Equal(types.EventTypeValidatorIncentivesReward, event.Type) + suite.Require().Len(event.Attributes, 2) + suite.Require().Equal(sdk.AttributeKeyAmount, event.Attributes[0].Key) + suite.Require().Equal(types.AttributeKeyValidator, event.Attributes[1].Key) + suite.Require().Equal(qualifyingVoters[i].Validator.GetOperator().String(), event.Attributes[1].Value) + } + } +} + +func (suite *KeeperTestSuite) TestAllocateTokensNoQualifyingVoters() { + ctx, incentivesKeeper := suite.ctx, suite.incentivesKeeper + + totalPreviousPower := int64(100) + totalDistribution := sdk.NewDecCoins(sdk.NewDecCoin("usom", sdk.NewInt(100))) + maxFraction := sdk.NewDecWithPrec(5, 1) // 0.5 + + // Call the function being tested with empty qualifyingVoters + remaining := incentivesKeeper.AllocateTokens(ctx, totalPreviousPower, totalDistribution, []ValidatorInfo{}, maxFraction) + + // Verify that all tokens remain unallocated + suite.Require().Equal(totalDistribution, remaining, "All tokens should remain unallocated when there are no qualifying voters") + + // Verify that no events were emitted + events := ctx.EventManager().Events() + suite.Require().Len(events, 0, "No events should be emitted when there are no qualifying voters") +} diff --git a/x/incentives/keeper/keeper.go b/x/incentives/keeper/keeper.go index dd812b80d..d3d5690f2 100644 --- a/x/incentives/keeper/keeper.go +++ b/x/incentives/keeper/keeper.go @@ -17,6 +17,7 @@ type Keeper struct { DistributionKeeper types.DistributionKeeper BankKeeper types.BankKeeper MintKeeper types.MintKeeper + StakingKeeper types.StakingKeeper } func NewKeeper( @@ -26,6 +27,7 @@ func NewKeeper( distributionKeeper types.DistributionKeeper, bankKeeper types.BankKeeper, mintKeeper types.MintKeeper, + stakingKeeper types.StakingKeeper, ) Keeper { if !paramSpace.HasKeyTable() { paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable()) @@ -38,6 +40,7 @@ func NewKeeper( DistributionKeeper: distributionKeeper, BankKeeper: bankKeeper, MintKeeper: mintKeeper, + StakingKeeper: stakingKeeper, } } diff --git a/x/incentives/keeper/keeper_test.go b/x/incentives/keeper/keeper_test.go index c04092516..e690c9ef0 100644 --- a/x/incentives/keeper/keeper_test.go +++ b/x/incentives/keeper/keeper_test.go @@ -26,6 +26,7 @@ type KeeperTestSuite struct { distributionKeeper *incentivestestutil.MockDistributionKeeper bankKeeper *incentivestestutil.MockBankKeeper mintKeeper *incentivestestutil.MockMintKeeper + stakingKeeper *incentivestestutil.MockStakingKeeper encCfg moduletestutil.TestEncodingConfig } @@ -44,6 +45,7 @@ func (suite *KeeperTestSuite) SetupTest() { suite.distributionKeeper = incentivestestutil.NewMockDistributionKeeper(ctrl) suite.bankKeeper = incentivestestutil.NewMockBankKeeper(ctrl) suite.mintKeeper = incentivestestutil.NewMockMintKeeper(ctrl) + suite.stakingKeeper = incentivestestutil.NewMockStakingKeeper(ctrl) suite.ctx = ctx params := paramskeeper.NewKeeper( @@ -64,6 +66,7 @@ func (suite *KeeperTestSuite) SetupTest() { suite.distributionKeeper, suite.bankKeeper, suite.mintKeeper, + suite.stakingKeeper, ) suite.encCfg = encCfg @@ -93,10 +96,10 @@ func (suite *KeeperTestSuite) TestGetAPY() { require.Equal(sdk.ZeroDec(), incentivesKeeper.GetAPY(ctx)) // incentives enabled - incentivesKeeper.SetParams(ctx, incentivesTypes.Params{ - DistributionPerBlock: distributionPerBlock, - IncentivesCutoffHeight: 1000, - }) + params := incentivesKeeper.GetParamSet(ctx) + params.DistributionPerBlock = distributionPerBlock + params.IncentivesCutoffHeight = 1000 + incentivesKeeper.SetParams(ctx, params) expected := sdk.NewDecFromInt(distributionPerBlock.Amount.Mul(sdk.NewInt(int64(blocksPerYear)))).Quo(sdk.NewDecFromInt(stakingTotalSupply).Mul(bondedRatio)) require.Equal(expected, incentivesKeeper.GetAPY(ctx)) } diff --git a/x/incentives/module.go b/x/incentives/module.go index bc102a26c..234c128c2 100644 --- a/x/incentives/module.go +++ b/x/incentives/module.go @@ -106,7 +106,7 @@ func (AppModule) QuerierRoute() string { return types.QuerierRoute } // ConsensusVersion implements AppModule/ConsensusVersion. func (AppModule) ConsensusVersion() uint64 { - return 1 + return 2 } func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation { @@ -135,8 +135,8 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw } // BeginBlock returns the begin blocker for the incentives module. -func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { - am.keeper.BeginBlocker(ctx) +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + am.keeper.BeginBlocker(ctx, req) } // EndBlock returns the end blocker for the incentives module. diff --git a/x/incentives/testutil/expected_keepers_mocks.go b/x/incentives/testutil/expected_keepers_mocks.go index 4f6cad0ad..c87ea1611 100644 --- a/x/incentives/testutil/expected_keepers_mocks.go +++ b/x/incentives/testutil/expected_keepers_mocks.go @@ -7,9 +7,11 @@ package mock_types import ( reflect "reflect" + math "cosmossdk.io/math" types "github.com/cosmos/cosmos-sdk/types" types0 "github.com/cosmos/cosmos-sdk/x/distribution/types" types1 "github.com/cosmos/cosmos-sdk/x/mint/types" + types2 "github.com/cosmos/cosmos-sdk/x/staking/types" gomock "github.com/golang/mock/gomock" ) @@ -50,6 +52,48 @@ func (mr *MockDistributionKeeperMockRecorder) GetFeePool(ctx interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeePool", reflect.TypeOf((*MockDistributionKeeper)(nil).GetFeePool), ctx) } +// GetValidatorAccumulatedCommission mocks base method. +func (m *MockDistributionKeeper) GetValidatorAccumulatedCommission(ctx types.Context, valAddr types.ValAddress) types0.ValidatorAccumulatedCommission { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorAccumulatedCommission", ctx, valAddr) + ret0, _ := ret[0].(types0.ValidatorAccumulatedCommission) + return ret0 +} + +// GetValidatorAccumulatedCommission indicates an expected call of GetValidatorAccumulatedCommission. +func (mr *MockDistributionKeeperMockRecorder) GetValidatorAccumulatedCommission(ctx, valAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorAccumulatedCommission", reflect.TypeOf((*MockDistributionKeeper)(nil).GetValidatorAccumulatedCommission), ctx, valAddr) +} + +// GetValidatorCurrentRewards mocks base method. +func (m *MockDistributionKeeper) GetValidatorCurrentRewards(ctx types.Context, valAddr types.ValAddress) types0.ValidatorCurrentRewards { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorCurrentRewards", ctx, valAddr) + ret0, _ := ret[0].(types0.ValidatorCurrentRewards) + return ret0 +} + +// GetValidatorCurrentRewards indicates an expected call of GetValidatorCurrentRewards. +func (mr *MockDistributionKeeperMockRecorder) GetValidatorCurrentRewards(ctx, valAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorCurrentRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).GetValidatorCurrentRewards), ctx, valAddr) +} + +// GetValidatorOutstandingRewards mocks base method. +func (m *MockDistributionKeeper) GetValidatorOutstandingRewards(ctx types.Context, valAddr types.ValAddress) types0.ValidatorOutstandingRewards { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValidatorOutstandingRewards", ctx, valAddr) + ret0, _ := ret[0].(types0.ValidatorOutstandingRewards) + return ret0 +} + +// GetValidatorOutstandingRewards indicates an expected call of GetValidatorOutstandingRewards. +func (mr *MockDistributionKeeperMockRecorder) GetValidatorOutstandingRewards(ctx, valAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorOutstandingRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).GetValidatorOutstandingRewards), ctx, valAddr) +} + // SetFeePool mocks base method. func (m *MockDistributionKeeper) SetFeePool(ctx types.Context, feePool types0.FeePool) { m.ctrl.T.Helper() @@ -62,6 +106,42 @@ func (mr *MockDistributionKeeperMockRecorder) SetFeePool(ctx, feePool interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFeePool", reflect.TypeOf((*MockDistributionKeeper)(nil).SetFeePool), ctx, feePool) } +// SetValidatorAccumulatedCommission mocks base method. +func (m *MockDistributionKeeper) SetValidatorAccumulatedCommission(ctx types.Context, valAddr types.ValAddress, commission types0.ValidatorAccumulatedCommission) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetValidatorAccumulatedCommission", ctx, valAddr, commission) +} + +// SetValidatorAccumulatedCommission indicates an expected call of SetValidatorAccumulatedCommission. +func (mr *MockDistributionKeeperMockRecorder) SetValidatorAccumulatedCommission(ctx, valAddr, commission interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetValidatorAccumulatedCommission", reflect.TypeOf((*MockDistributionKeeper)(nil).SetValidatorAccumulatedCommission), ctx, valAddr, commission) +} + +// SetValidatorCurrentRewards mocks base method. +func (m *MockDistributionKeeper) SetValidatorCurrentRewards(ctx types.Context, valAddr types.ValAddress, rewards types0.ValidatorCurrentRewards) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetValidatorCurrentRewards", ctx, valAddr, rewards) +} + +// SetValidatorCurrentRewards indicates an expected call of SetValidatorCurrentRewards. +func (mr *MockDistributionKeeperMockRecorder) SetValidatorCurrentRewards(ctx, valAddr, rewards interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetValidatorCurrentRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).SetValidatorCurrentRewards), ctx, valAddr, rewards) +} + +// SetValidatorOutstandingRewards mocks base method. +func (m *MockDistributionKeeper) SetValidatorOutstandingRewards(ctx types.Context, valAddr types.ValAddress, rewards types0.ValidatorOutstandingRewards) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetValidatorOutstandingRewards", ctx, valAddr, rewards) +} + +// SetValidatorOutstandingRewards indicates an expected call of SetValidatorOutstandingRewards. +func (mr *MockDistributionKeeperMockRecorder) SetValidatorOutstandingRewards(ctx, valAddr, rewards interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetValidatorOutstandingRewards", reflect.TypeOf((*MockDistributionKeeper)(nil).SetValidatorOutstandingRewards), ctx, valAddr, rewards) +} + // MockBankKeeper is a mock of BankKeeper interface. type MockBankKeeper struct { ctrl *gomock.Controller @@ -249,10 +329,10 @@ func (mr *MockMintKeeperMockRecorder) GetParams(ctx interface{}) *gomock.Call { } // StakingTokenSupply mocks base method. -func (m *MockMintKeeper) StakingTokenSupply(ctx types.Context) types.Int { +func (m *MockMintKeeper) StakingTokenSupply(ctx types.Context) math.Int { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "StakingTokenSupply", ctx) - ret0, _ := ret[0].(types.Int) + ret0, _ := ret[0].(math.Int) return ret0 } @@ -261,3 +341,40 @@ func (mr *MockMintKeeperMockRecorder) StakingTokenSupply(ctx interface{}) *gomoc mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StakingTokenSupply", reflect.TypeOf((*MockMintKeeper)(nil).StakingTokenSupply), ctx) } + +// MockStakingKeeper is a mock of StakingKeeper interface. +type MockStakingKeeper struct { + ctrl *gomock.Controller + recorder *MockStakingKeeperMockRecorder +} + +// MockStakingKeeperMockRecorder is the mock recorder for MockStakingKeeper. +type MockStakingKeeperMockRecorder struct { + mock *MockStakingKeeper +} + +// NewMockStakingKeeper creates a new mock instance. +func NewMockStakingKeeper(ctrl *gomock.Controller) *MockStakingKeeper { + mock := &MockStakingKeeper{ctrl: ctrl} + mock.recorder = &MockStakingKeeperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStakingKeeper) EXPECT() *MockStakingKeeperMockRecorder { + return m.recorder +} + +// ValidatorByConsAddr mocks base method. +func (m *MockStakingKeeper) ValidatorByConsAddr(ctx types.Context, consAddr types.ConsAddress) types2.ValidatorI { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidatorByConsAddr", ctx, consAddr) + ret0, _ := ret[0].(types2.ValidatorI) + return ret0 +} + +// ValidatorByConsAddr indicates an expected call of ValidatorByConsAddr. +func (mr *MockStakingKeeperMockRecorder) ValidatorByConsAddr(ctx, consAddr interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidatorByConsAddr", reflect.TypeOf((*MockStakingKeeper)(nil).ValidatorByConsAddr), ctx, consAddr) +} diff --git a/x/incentives/types/errors.go b/x/incentives/types/errors.go index c59b21c9d..4677d4971 100644 --- a/x/incentives/types/errors.go +++ b/x/incentives/types/errors.go @@ -5,5 +5,8 @@ import ( ) var ( - ErrInvalidDistributionPerBlock = errorsmod.Register(ModuleName, 1, "invalid distribution per block") + ErrInvalidDistributionPerBlock = errorsmod.Register(ModuleName, 1, "invalid distribution per block") + ErrInvalidValidatorMaxDistributionPerBlock = errorsmod.Register(ModuleName, 2, "invalid validator distribution per block") + ErrInvalidValidatorIncentivesMaxFraction = errorsmod.Register(ModuleName, 3, "invalid validator incentives max fraction") + ErrInvalidValidatorIncentivesSetSizeLimit = errorsmod.Register(ModuleName, 4, "invalid validator incentives set size limit") ) diff --git a/x/incentives/types/events.go b/x/incentives/types/events.go new file mode 100644 index 000000000..09cecfb71 --- /dev/null +++ b/x/incentives/types/events.go @@ -0,0 +1,8 @@ +package types + +const ( + EventTypeValidatorIncentivesReward = "validator_incentives_reward" + EventTypeTotalValidatorIncentivesRewards = "total_validator_incentives_rewards" + + AttributeKeyValidator = "validator" +) diff --git a/x/incentives/types/expected_keepers.go b/x/incentives/types/expected_keepers.go index b270094f7..3d89312e3 100644 --- a/x/incentives/types/expected_keepers.go +++ b/x/incentives/types/expected_keepers.go @@ -7,12 +7,19 @@ import ( sdk "github.com/cosmos/cosmos-sdk/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" ) // DistributionKeeper defines the expected distribution keeper methods type DistributionKeeper interface { GetFeePool(ctx sdk.Context) (feePool distributiontypes.FeePool) SetFeePool(ctx sdk.Context, feePool distributiontypes.FeePool) + GetValidatorOutstandingRewards(ctx sdk.Context, valAddr sdk.ValAddress) (rewards distributiontypes.ValidatorOutstandingRewards) + SetValidatorOutstandingRewards(ctx sdk.Context, valAddr sdk.ValAddress, rewards distributiontypes.ValidatorOutstandingRewards) + GetValidatorCurrentRewards(ctx sdk.Context, valAddr sdk.ValAddress) (rewards distributiontypes.ValidatorCurrentRewards) + SetValidatorCurrentRewards(ctx sdk.Context, valAddr sdk.ValAddress, rewards distributiontypes.ValidatorCurrentRewards) + GetValidatorAccumulatedCommission(ctx sdk.Context, valAddr sdk.ValAddress) (commission distributiontypes.ValidatorAccumulatedCommission) + SetValidatorAccumulatedCommission(ctx sdk.Context, valAddr sdk.ValAddress, commission distributiontypes.ValidatorAccumulatedCommission) } // BankKeeper defines the expected interface needed to retrieve account balances. @@ -35,3 +42,7 @@ type MintKeeper interface { StakingTokenSupply(ctx sdk.Context) math.Int BondedRatio(ctx sdk.Context) sdk.Dec } + +type StakingKeeper interface { + ValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) stakingtypes.ValidatorI +} diff --git a/x/incentives/types/genesis.pb.go b/x/incentives/types/genesis.pb.go index 42b1b63c0..febfc9bfb 100644 --- a/x/incentives/types/genesis.pb.go +++ b/x/incentives/types/genesis.pb.go @@ -5,6 +5,7 @@ package types import ( fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" @@ -75,6 +76,14 @@ type Params struct { // IncentivesCutoffHeight defines the block height after which the incentives module will stop sending coins to the distribution module from // the community pool IncentivesCutoffHeight uint64 `protobuf:"varint,2,opt,name=incentives_cutoff_height,json=incentivesCutoffHeight,proto3" json:"incentives_cutoff_height,omitempty"` + // ValidatorMaxDistributionPerBlock defines the maximum coins to be sent directly to voters in the last block from the community pool every block. Leftover coins remain in the community pool. + ValidatorMaxDistributionPerBlock types.Coin `protobuf:"bytes,3,opt,name=validator_max_distribution_per_block,json=validatorMaxDistributionPerBlock,proto3" json:"validator_max_distribution_per_block"` + // ValidatorIncentivesCutoffHeight defines the block height after which the validator incentives will be stopped + ValidatorIncentivesCutoffHeight uint64 `protobuf:"varint,4,opt,name=validator_incentives_cutoff_height,json=validatorIncentivesCutoffHeight,proto3" json:"validator_incentives_cutoff_height,omitempty"` + // ValidatorIncentivesMaxFraction defines the maximum fraction of the validator distribution per block that can be sent to a single validator + ValidatorIncentivesMaxFraction github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=validator_incentives_max_fraction,json=validatorIncentivesMaxFraction,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"validator_incentives_max_fraction"` + // ValidatorIncentivesSetSizeLimit defines the max number of validators to apportion the validator distribution per block to + ValidatorIncentivesSetSizeLimit uint64 `protobuf:"varint,6,opt,name=validator_incentives_set_size_limit,json=validatorIncentivesSetSizeLimit,proto3" json:"validator_incentives_set_size_limit,omitempty"` } func (m *Params) Reset() { *m = Params{} } @@ -124,6 +133,27 @@ func (m *Params) GetIncentivesCutoffHeight() uint64 { return 0 } +func (m *Params) GetValidatorMaxDistributionPerBlock() types.Coin { + if m != nil { + return m.ValidatorMaxDistributionPerBlock + } + return types.Coin{} +} + +func (m *Params) GetValidatorIncentivesCutoffHeight() uint64 { + if m != nil { + return m.ValidatorIncentivesCutoffHeight + } + return 0 +} + +func (m *Params) GetValidatorIncentivesSetSizeLimit() uint64 { + if m != nil { + return m.ValidatorIncentivesSetSizeLimit + } + return 0 +} + func init() { proto.RegisterType((*GenesisState)(nil), "incentives.v1.GenesisState") proto.RegisterType((*Params)(nil), "incentives.v1.Params") @@ -132,27 +162,35 @@ func init() { func init() { proto.RegisterFile("incentives/v1/genesis.proto", fileDescriptor_179cfb82d3e2b395) } var fileDescriptor_179cfb82d3e2b395 = []byte{ - // 309 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0xd0, 0xb1, 0x4f, 0x3a, 0x31, - 0x14, 0x07, 0xf0, 0xeb, 0x2f, 0x84, 0xe1, 0x7e, 0xba, 0x5c, 0x90, 0x20, 0x26, 0x95, 0x30, 0x31, - 0xb5, 0x39, 0x18, 0x74, 0x86, 0x41, 0x07, 0x07, 0x82, 0x71, 0x71, 0xb9, 0xdc, 0xd5, 0x47, 0xa9, - 0x72, 0x7d, 0x97, 0xb6, 0x34, 0xf2, 0x5f, 0xb8, 0xfa, 0x1f, 0x31, 0x32, 0x3a, 0x19, 0x03, 0xff, - 0x88, 0xe1, 0xee, 0x12, 0x70, 0x6b, 0xf2, 0x79, 0xef, 0xdb, 0x6f, 0x5e, 0x78, 0xa5, 0xb4, 0x00, - 0xed, 0x94, 0x07, 0xcb, 0x7d, 0xcc, 0x25, 0x68, 0xb0, 0xca, 0xb2, 0xc2, 0xa0, 0xc3, 0xe8, 0xfc, - 0x88, 0xcc, 0xc7, 0xdd, 0x96, 0x44, 0x89, 0xa5, 0xf0, 0xc3, 0xab, 0x1a, 0xea, 0x52, 0x81, 0x36, - 0x47, 0xcb, 0xb3, 0xd4, 0x02, 0xf7, 0x71, 0x06, 0x2e, 0x8d, 0xb9, 0x40, 0xa5, 0x2b, 0xef, 0x4f, - 0xc2, 0xb3, 0xbb, 0x2a, 0xf5, 0xd1, 0xa5, 0x0e, 0xa2, 0x51, 0xd8, 0x2c, 0x52, 0x93, 0xe6, 0xb6, - 0x43, 0x7a, 0x64, 0xf0, 0x7f, 0x78, 0xc1, 0xfe, 0xfc, 0xc2, 0xa6, 0x25, 0x8e, 0x1b, 0x9b, 0xef, - 0xeb, 0x60, 0x56, 0x8f, 0xf6, 0x3f, 0x49, 0xd8, 0xac, 0x20, 0x7a, 0x0a, 0xdb, 0x2f, 0xca, 0x3a, - 0xa3, 0xb2, 0x95, 0x53, 0xa8, 0x93, 0x02, 0x4c, 0x92, 0x2d, 0x51, 0xbc, 0xd5, 0x79, 0x97, 0xac, - 0x2a, 0xc4, 0x0e, 0x85, 0x58, 0x5d, 0x88, 0x4d, 0x50, 0xe9, 0x3a, 0xb3, 0x75, 0xba, 0x3e, 0x05, - 0x33, 0x3e, 0x2c, 0x47, 0xb7, 0x61, 0xe7, 0xd8, 0x23, 0x11, 0x2b, 0x87, 0xf3, 0x79, 0xb2, 0x00, - 0x25, 0x17, 0xae, 0xf3, 0xaf, 0x47, 0x06, 0x8d, 0x59, 0xfb, 0xe8, 0x93, 0x92, 0xef, 0x4b, 0x1d, - 0x3f, 0x6c, 0x76, 0x94, 0x6c, 0x77, 0x94, 0xfc, 0xec, 0x28, 0xf9, 0xd8, 0xd3, 0x60, 0xbb, 0xa7, - 0xc1, 0xd7, 0x9e, 0x06, 0xcf, 0x43, 0xa9, 0xdc, 0x62, 0x95, 0x31, 0x81, 0x39, 0x2f, 0x40, 0xca, - 0xf5, 0xab, 0xe7, 0x16, 0xf3, 0x1c, 0x96, 0x0a, 0x0c, 0xf7, 0x37, 0xfc, 0x9d, 0x9f, 0x9c, 0xdf, - 0xad, 0x0b, 0xb0, 0x59, 0xb3, 0xbc, 0xda, 0xe8, 0x37, 0x00, 0x00, 0xff, 0xff, 0xa1, 0x68, 0x83, - 0x05, 0x99, 0x01, 0x00, 0x00, + // 442 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0x4d, 0x8b, 0xd3, 0x40, + 0x18, 0xc7, 0x1b, 0xad, 0x05, 0x47, 0xbd, 0x84, 0x75, 0x89, 0x2b, 0xa4, 0xb5, 0x8a, 0xf4, 0xe2, + 0x0c, 0xdd, 0x3d, 0xe8, 0xb9, 0x5d, 0x7c, 0xc1, 0x2e, 0x2c, 0x2d, 0x5e, 0xbc, 0x0c, 0x93, 0xe9, + 0xd3, 0xf4, 0x71, 0x9b, 0x4c, 0x98, 0x99, 0x86, 0x76, 0x3f, 0x85, 0x1f, 0xc1, 0x8f, 0xb3, 0xc7, + 0x3d, 0x8a, 0x87, 0x45, 0xda, 0x2f, 0x22, 0x79, 0xa1, 0xa9, 0x90, 0xc2, 0x9e, 0x12, 0xf8, 0x3d, + 0xf3, 0xff, 0xff, 0x1e, 0x92, 0x21, 0x2f, 0x31, 0x96, 0x10, 0x5b, 0x4c, 0xc1, 0xb0, 0xb4, 0xcf, + 0x42, 0x88, 0xc1, 0xa0, 0xa1, 0x89, 0x56, 0x56, 0xb9, 0xcf, 0x2a, 0x48, 0xd3, 0xfe, 0xc9, 0x51, + 0xa8, 0x42, 0x95, 0x13, 0x96, 0xbd, 0x15, 0x43, 0x27, 0xbe, 0x54, 0x26, 0x52, 0x86, 0x05, 0xc2, + 0x00, 0x4b, 0xfb, 0x01, 0x58, 0xd1, 0x67, 0x52, 0x61, 0x5c, 0xf0, 0xee, 0x90, 0x3c, 0xfd, 0x54, + 0xa4, 0x4e, 0xac, 0xb0, 0xe0, 0x9e, 0x91, 0x56, 0x22, 0xb4, 0x88, 0x8c, 0xe7, 0x74, 0x9c, 0xde, + 0x93, 0xd3, 0xe7, 0xf4, 0xbf, 0x16, 0x7a, 0x99, 0xc3, 0x41, 0xf3, 0xe6, 0xae, 0xdd, 0x18, 0x97, + 0xa3, 0xdd, 0x5f, 0x4d, 0xd2, 0x2a, 0x80, 0xfb, 0x8d, 0x1c, 0x4f, 0xd1, 0x58, 0x8d, 0xc1, 0xd2, + 0xa2, 0x8a, 0x79, 0x02, 0x9a, 0x07, 0x0b, 0x25, 0xaf, 0xca, 0xbc, 0x17, 0xb4, 0x10, 0xa2, 0x99, + 0x10, 0x2d, 0x85, 0xe8, 0x50, 0x61, 0x5c, 0x66, 0x1e, 0xed, 0x1f, 0xbf, 0x04, 0x3d, 0xc8, 0x0e, + 0xbb, 0x1f, 0x88, 0x57, 0x79, 0x70, 0xb9, 0xb4, 0x6a, 0x36, 0xe3, 0x73, 0xc0, 0x70, 0x6e, 0xbd, + 0x07, 0x1d, 0xa7, 0xd7, 0x1c, 0x1f, 0x57, 0x7c, 0x98, 0xe3, 0xcf, 0x39, 0x75, 0x15, 0x79, 0x93, + 0x8a, 0x05, 0x4e, 0x85, 0x55, 0x9a, 0x47, 0x62, 0xc5, 0x0f, 0xe8, 0x3d, 0xbc, 0x9f, 0x5e, 0x67, + 0x17, 0x76, 0x21, 0x56, 0xe7, 0x75, 0xaa, 0x5f, 0x49, 0xb7, 0x2a, 0x3c, 0x28, 0xdd, 0xcc, 0xa5, + 0xdb, 0xbb, 0xc9, 0x2f, 0xf5, 0xf6, 0x6b, 0xf2, 0xaa, 0x36, 0x2c, 0x5b, 0x64, 0xa6, 0x85, 0xcc, + 0x9a, 0xbd, 0x47, 0x1d, 0xa7, 0xf7, 0x78, 0x40, 0x33, 0xbf, 0x3f, 0x77, 0xed, 0xb7, 0x21, 0xda, + 0xf9, 0x32, 0xa0, 0x52, 0x45, 0xac, 0xfc, 0xf8, 0xc5, 0xe3, 0x9d, 0x99, 0x5e, 0x31, 0xbb, 0x4e, + 0xc0, 0xd0, 0x73, 0x90, 0x63, 0xbf, 0xa6, 0xfb, 0x42, 0xac, 0x3e, 0x96, 0xa9, 0xee, 0x88, 0xbc, + 0xae, 0xad, 0x36, 0x60, 0xb9, 0xc1, 0x6b, 0xe0, 0x0b, 0x8c, 0xd0, 0x7a, 0xad, 0x83, 0x8b, 0x4c, + 0xc0, 0x4e, 0xf0, 0x1a, 0x46, 0xd9, 0xd8, 0x60, 0x74, 0xb3, 0xf1, 0x9d, 0xdb, 0x8d, 0xef, 0xfc, + 0xdd, 0xf8, 0xce, 0xcf, 0xad, 0xdf, 0xb8, 0xdd, 0xfa, 0x8d, 0xdf, 0x5b, 0xbf, 0xf1, 0xfd, 0x74, + 0xcf, 0x37, 0x81, 0x30, 0x5c, 0xff, 0x48, 0x99, 0x51, 0x51, 0x04, 0x0b, 0x04, 0xcd, 0xd2, 0xf7, + 0x6c, 0xc5, 0xf6, 0x6e, 0x41, 0xee, 0x1f, 0xb4, 0xf2, 0x9f, 0xf7, 0xec, 0x5f, 0x00, 0x00, 0x00, + 0xff, 0xff, 0x32, 0x0e, 0x87, 0xd4, 0x20, 0x03, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -208,6 +246,36 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.ValidatorIncentivesSetSizeLimit != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.ValidatorIncentivesSetSizeLimit)) + i-- + dAtA[i] = 0x30 + } + { + size := m.ValidatorIncentivesMaxFraction.Size() + i -= size + if _, err := m.ValidatorIncentivesMaxFraction.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + if m.ValidatorIncentivesCutoffHeight != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.ValidatorIncentivesCutoffHeight)) + i-- + dAtA[i] = 0x20 + } + { + size, err := m.ValidatorMaxDistributionPerBlock.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a if m.IncentivesCutoffHeight != 0 { i = encodeVarintGenesis(dAtA, i, uint64(m.IncentivesCutoffHeight)) i-- @@ -259,6 +327,16 @@ func (m *Params) Size() (n int) { if m.IncentivesCutoffHeight != 0 { n += 1 + sovGenesis(uint64(m.IncentivesCutoffHeight)) } + l = m.ValidatorMaxDistributionPerBlock.Size() + n += 1 + l + sovGenesis(uint64(l)) + if m.ValidatorIncentivesCutoffHeight != 0 { + n += 1 + sovGenesis(uint64(m.ValidatorIncentivesCutoffHeight)) + } + l = m.ValidatorIncentivesMaxFraction.Size() + n += 1 + l + sovGenesis(uint64(l)) + if m.ValidatorIncentivesSetSizeLimit != 0 { + n += 1 + sovGenesis(uint64(m.ValidatorIncentivesSetSizeLimit)) + } return n } @@ -432,6 +510,111 @@ func (m *Params) Unmarshal(dAtA []byte) error { break } } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorMaxDistributionPerBlock", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ValidatorMaxDistributionPerBlock.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorIncentivesCutoffHeight", wireType) + } + m.ValidatorIncentivesCutoffHeight = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ValidatorIncentivesCutoffHeight |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorIncentivesMaxFraction", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.ValidatorIncentivesMaxFraction.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorIncentivesSetSizeLimit", wireType) + } + m.ValidatorIncentivesSetSizeLimit = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ValidatorIncentivesSetSizeLimit |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/incentives/types/params.go b/x/incentives/types/params.go index f434d5744..9927a1383 100644 --- a/x/incentives/types/params.go +++ b/x/incentives/types/params.go @@ -9,8 +9,12 @@ import ( // Parameter keys var ( - KeyDistributionPerBlock = []byte("DistributionPerBlock") - KeyIncentivesCutoffHeight = []byte("IncentivesCutoffHeight") + KeyDistributionPerBlock = []byte("DistributionPerBlock") + KeyIncentivesCutoffHeight = []byte("IncentivesCutoffHeight") + KeyValidatorMaxDistributionPerBlock = []byte("ValidatorMaxDistributionPerBlock") + KeyValidatorIncentivesCutoffHeight = []byte("ValidatorIncentivesCutoffHeight") + KeyValidatorIncentivesMaxFraction = []byte("ValidatorIncentivesMaxFraction") + KeyValidatorIncentivesSetSizeLimit = []byte("ValidatorIncentivesSetSizeLimit") ) var _ paramtypes.ParamSet = &Params{} @@ -25,7 +29,11 @@ func DefaultParams() Params { return Params{ DistributionPerBlock: sdk.NewCoin(params.BaseCoinUnit, sdk.ZeroInt()), // Anything lower than or equal to current height is "off" - IncentivesCutoffHeight: 0, + IncentivesCutoffHeight: 0, + ValidatorMaxDistributionPerBlock: sdk.NewCoin(params.BaseCoinUnit, sdk.ZeroInt()), + ValidatorIncentivesCutoffHeight: 0, + ValidatorIncentivesMaxFraction: sdk.MustNewDecFromStr("0.1"), + ValidatorIncentivesSetSizeLimit: 50, } } @@ -34,6 +42,10 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { return paramtypes.ParamSetPairs{ paramtypes.NewParamSetPair(KeyDistributionPerBlock, &p.DistributionPerBlock, validateDistributionPerBlock), paramtypes.NewParamSetPair(KeyIncentivesCutoffHeight, &p.IncentivesCutoffHeight, validateIncentivesCutoffHeight), + paramtypes.NewParamSetPair(KeyValidatorMaxDistributionPerBlock, &p.ValidatorMaxDistributionPerBlock, validateValidatorMaxDistributionPerBlock), + paramtypes.NewParamSetPair(KeyValidatorIncentivesCutoffHeight, &p.ValidatorIncentivesCutoffHeight, validateValidatorIncentivesCutoffHeight), + paramtypes.NewParamSetPair(KeyValidatorIncentivesMaxFraction, &p.ValidatorIncentivesMaxFraction, validateValidatorIncentivesMaxFraction), + paramtypes.NewParamSetPair(KeyValidatorIncentivesSetSizeLimit, &p.ValidatorIncentivesSetSizeLimit, validateValidatorIncentivesSetSizeLimit), } } @@ -69,3 +81,41 @@ func validateDistributionPerBlock(i interface{}) error { func validateIncentivesCutoffHeight(_ interface{}) error { return nil } + +func validateValidatorMaxDistributionPerBlock(i interface{}) error { + if err := validateDistributionPerBlock(i); err != nil { + return errorsmod.Wrapf(ErrInvalidValidatorMaxDistributionPerBlock, "invalid parameter type: %T", i) + } + + return nil +} + +func validateValidatorIncentivesCutoffHeight(i interface{}) error { + return nil +} + +func validateValidatorIncentivesMaxFraction(i interface{}) error { + dec, ok := i.(sdk.Dec) + if !ok { + return errorsmod.Wrapf(ErrInvalidValidatorIncentivesMaxFraction, "invalid parameter type: %T", i) + } + + if dec.IsNegative() { + return errorsmod.Wrapf(ErrInvalidValidatorIncentivesMaxFraction, "validator incentives max fraction cannot be negative") + } + + if dec.GT(sdk.OneDec()) { + return errorsmod.Wrapf(ErrInvalidValidatorIncentivesMaxFraction, "validator incentives max fraction cannot be greater than one") + } + + return nil +} + +func validateValidatorIncentivesSetSizeLimit(i interface{}) error { + _, ok := i.(uint64) + if !ok { + return errorsmod.Wrapf(ErrInvalidValidatorIncentivesSetSizeLimit, "invalid parameter type: %T", i) + } + + return nil +}