diff --git a/tests/integration/distribution.go b/tests/integration/distribution.go index 6a680fce8f..03fb771867 100644 --- a/tests/integration/distribution.go +++ b/tests/integration/distribution.go @@ -4,6 +4,7 @@ import ( "strings" "cosmossdk.io/math" + abci "github.com/cometbft/cometbft/abci/types" "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" @@ -601,6 +602,105 @@ func (s *CCVTestSuite) TestIBCTransferMiddleware() { } } +// TestAllocateTokens is a happy-path test of the consumer rewards pool allocation +// to opted-in validators and the community pool +func (s *CCVTestSuite) TestAllocateTokens() { + // set up channel and delegate some tokens in order for validator set update to be sent to the consumer chain + s.SetupAllCCVChannels() + providerKeeper := s.providerApp.GetProviderKeeper() + bankKeeper := s.providerApp.GetTestBankKeeper() + distributionKeeper := s.providerApp.GetTestDistributionKeeper() + + totalRewards := sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))} + providerKeeper.SetConsumerRewardDenom(s.providerCtx(), sdk.DefaultBondDenom) // register a consumer reward denom + + // fund consumer rewards pool + bankKeeper.SendCoinsFromAccountToModule( + s.providerCtx(), + s.providerChain.SenderAccount.GetAddress(), + providertypes.ConsumerRewardsPool, + totalRewards, + ) + + // Allocate rewards evenly between consumers + rewardsPerConsumer := totalRewards.QuoInt(math.NewInt(int64(len(s.consumerBundles)))) + for chainID := range s.consumerBundles { + // update consumer allocation + providerKeeper.SetConsumerRewardsAllocation( + s.providerCtx(), + chainID, + providertypes.ConsumerRewardsAllocation{ + Rewards: sdk.NewDecCoinsFromCoins(rewardsPerConsumer...), + }, + ) + + // opt in all validators for the chain + for _, val := range s.providerChain.Vals.Validators { + providerKeeper.SetOptedIn( + s.providerCtx(), + chainID, + providertypes.NewProviderConsAddress(sdk.ConsAddress(val.Address)), + uint64(s.providerCtx().BlockHeight()), + ) + } + } + + // Iterate over the validators and + // store their current voting power and outstanding rewards + lastValOutRewards := map[string]sdk.DecCoins{} + votes := []abci.VoteInfo{} + for _, val := range s.providerChain.Vals.Validators { + votes = append(votes, + abci.VoteInfo{ + Validator: abci.Validator{Address: val.Address, Power: val.VotingPower}, + SignedLastBlock: true, + }, + ) + + valRewards := distributionKeeper.GetValidatorOutstandingRewards(s.providerCtx(), sdk.ValAddress(val.Address)) + lastValOutRewards[sdk.ValAddress(val.Address).String()] = valRewards.Rewards + } + + // store community pool balance + lastCommPool := distributionKeeper.GetFeePoolCommunityCoins(s.providerCtx()) + + // execute BeginBlock to trigger the token allocation + providerKeeper.BeginBlockRD( + s.providerCtx(), + abci.RequestBeginBlock{ + LastCommitInfo: abci.CommitInfo{ + Votes: votes, + }, + }, + ) + + valNum := len(s.providerChain.Vals.Validators) + consuNum := len(s.consumerBundles) + + // compute the expected validators token allocation by subtracting the community tax + rewardsPerConsumerDec := sdk.NewDecCoinsFromCoins(rewardsPerConsumer...) + communityTax := distributionKeeper.GetCommunityTax(s.providerCtx()) + validatorsExpRewards := rewardsPerConsumerDec. + MulDecTruncate(math.LegacyOneDec().Sub(communityTax)). + // multiply by the number of consumers since all the validators opted in + MulDec(sdk.NewDec(int64(consuNum))) + perValExpReward := validatorsExpRewards.QuoDec(sdk.NewDec(int64(valNum))) + + // verify the validator tokens allocation + // note all validators have the same voting power to keep things simple + for _, val := range s.providerChain.Vals.Validators { + valReward := distributionKeeper.GetValidatorOutstandingRewards(s.providerCtx(), sdk.ValAddress(val.Address)) + s.Require().True(valReward.Rewards.IsEqual( + lastValOutRewards[sdk.ValAddress(val.Address).String()].Add(perValExpReward...), + )) + } + + commPoolExpRewards := sdk.NewDecCoinsFromCoins(totalRewards...).Sub(validatorsExpRewards) + currCommPool := distributionKeeper.GetFeePoolCommunityCoins(s.providerCtx()) + + s.Require().True(currCommPool.IsEqual(lastCommPool.Add(commPoolExpRewards...))) +} + // getEscrowBalance gets the current balances in the escrow account holding the transferred tokens to the provider func (s *CCVTestSuite) getEscrowBalance() sdk.Coins { consumerBankKeeper := s.consumerApp.GetTestBankKeeper() diff --git a/testutil/integration/debug_test.go b/testutil/integration/debug_test.go index 79462c7b41..60c996a245 100644 --- a/testutil/integration/debug_test.go +++ b/testutil/integration/debug_test.go @@ -287,3 +287,7 @@ func TestSlashRetries(t *testing.T) { func TestIBCTransferMiddleware(t *testing.T) { runCCVTestByName(t, "TestIBCTransferMiddleware") } + +func TestAllocateTokens(t *testing.T) { + runCCVTestByName(t, "TestAllocateTokens") +} diff --git a/x/ccv/provider/keeper/distribution.go b/x/ccv/provider/keeper/distribution.go index 46a9abb9e4..b014b1eaf5 100644 --- a/x/ccv/provider/keeper/distribution.go +++ b/x/ccv/provider/keeper/distribution.go @@ -11,7 +11,6 @@ import ( // BeginBlockRD executes BeginBlock logic for the Reward Distribution sub-protocol. func (k Keeper) BeginBlockRD(ctx sdk.Context, req abci.RequestBeginBlock) { - // transfers all whitelisted consumer rewards to the fee collector address // determine the total power signing the block var previousTotalPower int64 @@ -125,34 +124,43 @@ func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64, bonded } // TODO: define which validators earned the tokens, i.e. already opted in for 1000 blocks +// AllocateTokensToConsumerValidators allocates the consumer rewards pool to validator +// according to their voting power func (k Keeper) AllocateTokensToConsumerValidators( ctx sdk.Context, chainID string, totalPreviousPower int64, bondedVotes []abci.VoteInfo, - fees sdk.DecCoins, -) (totalReward sdk.DecCoins) { - for _, vote := range bondedVotes { - valConsAddr := vote.Validator.Address + tokens sdk.DecCoins, +) sdk.DecCoins { + optedInVals := map[string]struct{}{} + for _, val := range k.GetOptedIn(ctx, chainID) { + optedInVals[val.ProviderAddr.Address.String()] = struct{}{} + } - if _, found := k.GetValidatorConsumerPubKey(ctx, chainID, types.NewProviderConsAddress(valConsAddr)); !found { + totalReward := sdk.DecCoins{} + + for _, vote := range bondedVotes { + consAddr := sdk.ConsAddress(vote.Validator.Address) + if _, ok := optedInVals[consAddr.String()]; !ok { continue } + // TODO: Consider micro-slashing for missing votes. // // Ref: https://github.com/cosmos/cosmos-sdk/issues/2525#issuecomment-430838701 powerFraction := math.LegacyNewDec(vote.Validator.Power).QuoTruncate(math.LegacyNewDec(totalPreviousPower)) - feeFraction := fees.MulDecTruncate(powerFraction) + tokensFraction := tokens.MulDecTruncate(powerFraction) k.distributionKeeper.AllocateTokensToValidator( ctx, - k.stakingKeeper.ValidatorByConsAddr(ctx, valConsAddr), - feeFraction, + k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr), + tokensFraction, ) - totalReward = totalReward.Add(feeFraction...) + totalReward = totalReward.Add(tokensFraction...) } - return + return totalReward } // TransferConsumerRewardsToDistributionModule transfers the collected rewards of the given consumer chain @@ -161,7 +169,6 @@ func (k Keeper) TransferConsumerRewardsToDistributionModule( ctx sdk.Context, chainID string, ) (sdk.Coins, error) { - // Get coins of the consumer reward pool pool := k.GetConsumerRewardsAllocation(ctx, chainID) @@ -200,7 +207,7 @@ func (k Keeper) TransferConsumerRewardsToDistributionModule( // consumer reward pools getter and setter -// get the consumer reward pool distribution info +// GetConsumerRewardsAllocation the onsumer rewards allocation for the given chain ID func (k Keeper) GetConsumerRewardsAllocation(ctx sdk.Context, chainID string) (pool types.ConsumerRewardsAllocation) { store := ctx.KVStore(k.storeKey) b := store.Get(types.ConsumerRewardPoolKey(chainID)) @@ -208,7 +215,7 @@ func (k Keeper) GetConsumerRewardsAllocation(ctx sdk.Context, chainID string) (p return } -// set the consumer reward pool distribution info +// SetConsumerRewardsAllocation sets the consumer rewards allocation for the given chain ID func (k Keeper) SetConsumerRewardsAllocation(ctx sdk.Context, chainID string, pool types.ConsumerRewardsAllocation) { store := ctx.KVStore(k.storeKey) b := k.cdc.MustMarshal(&pool)