From 8adba69efc1be30845be8a8e55f21a372c60e72e Mon Sep 17 00:00:00 2001 From: Simon Noetzlin Date: Fri, 2 Feb 2024 15:58:25 +0100 Subject: [PATCH] save --- app/provider/app.go | 41 ++-- reward-distribution.md | 62 ++++++ tests/integration/distribution.go | 309 +++++++++++++++++++++++++- testutil/ibc_testing/generic_setup.go | 3 + testutil/integration/debug_test.go | 4 + testutil/integration/interfaces.go | 1 + testutil/keeper/mocks.go | 155 ++++++++----- x/ccv/provider/ibc_module.go | 12 +- x/ccv/provider/ibc_module_test.go | 11 +- x/ccv/provider/keeper/distribution.go | 177 ++++++++++++--- x/ccv/provider/keeper/keeper.go | 37 ++- x/ccv/provider/module.go | 4 +- x/ccv/provider/types/keys.go | 27 ++- x/ccv/provider/types/keys_test.go | 2 + x/ccv/types/expected_keepers.go | 6 + 15 files changed, 728 insertions(+), 123 deletions(-) create mode 100644 reward-distribution.md diff --git a/app/provider/app.go b/app/provider/app.go index 7527756d45..9702fadd0e 100644 --- a/app/provider/app.go +++ b/app/provider/app.go @@ -158,14 +158,13 @@ var ( // module account permissions maccPerms = map[string][]string{ - authtypes.FeeCollectorName: nil, - distrtypes.ModuleName: nil, - minttypes.ModuleName: {authtypes.Minter}, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, - govtypes.ModuleName: {authtypes.Burner}, - ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, - providertypes.ConsumerRewardsPool: nil, + authtypes.FeeCollectorName: nil, + distrtypes.ModuleName: nil, + minttypes.ModuleName: {authtypes.Minter}, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + govtypes.ModuleName: {authtypes.Burner}, + ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, } ) @@ -300,6 +299,10 @@ func New( scopedIBCProviderKeeper := app.CapabilityKeeper.ScopeToModule(providertypes.ModuleName) app.CapabilityKeeper.Seal() + // add consumer chain module accounts to module account permissions + // this MUST be called before the AccountKeeper initialisation + app.addConsumerChainAccountToMacPerms() + // add keepers app.AccountKeeper = authkeeper.NewAccountKeeper( appCodec, @@ -310,12 +313,10 @@ func New( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) - // Remove the ConsumerRewardsPool from the group of blocked recipient addresses in bank + // Remove the consumer module accounts from the group of blocked recipient addresses in bank // this is required for the provider chain to be able to receive tokens from - // the consumer chain - bankBlockedAddrs := app.ModuleAccountAddrs() - delete(bankBlockedAddrs, authtypes.NewModuleAddress( - providertypes.ConsumerRewardsPool).String()) + // consumer chains + bankBlockedAddrs := app.BlockedModuleAccountAddrs() app.BankKeeper = bankkeeper.NewBaseKeeper( appCodec, @@ -947,3 +948,17 @@ func makeEncodingConfig() appencoding.EncodingConfig { ModuleBasics.RegisterInterfaces(encodingConfig.InterfaceRegistry) return encodingConfig } + +func (app *App) addConsumerChainAccountToMacPerms() { + for _, c := range providertypes.ConsumerRewardPools { + maccPerms[c] = nil + } +} + +func (app *App) BlockedModuleAccountAddrs() map[string]bool { + acctAddrs := app.ModuleAccountAddrs() + for _, c := range providertypes.ConsumerRewardPools { + delete(acctAddrs, c) + } + return acctAddrs +} diff --git a/reward-distribution.md b/reward-distribution.md new file mode 100644 index 0000000000..5609b5f201 --- /dev/null +++ b/reward-distribution.md @@ -0,0 +1,62 @@ + +# PSS: reward distribution design proposal + +## Provider states + +### x/ccv/provider + +- in-store: consumer chain Id -> opted-in validators mapping +- in-store: consumer chain Id -> module account (reward fee pool) mapping +- code constant: consumer reward pools / module account name list +- in-store: last consumer module account index assigned + +### x/banking + +- consumer module account balances +- community fee pool balance (x/distr module account balance) + +### x/auth + +- consumer module accounts + +### ABCI + +- previous block validator votes used as an input to calculate the validators reward allocations (abci.CommitInfo) + +### IBC packet + +- handshake metadata storing the consumer reward fee pool, i.e. assigned module account on the provider + +### Actions + +- Provider app startup: + + - Module accounts are created from the consumer reward pool list + +- Provider App runtime: + + - for each consumer addition proposal that passes, assign a reward pool to the associated consumer chain + - when a consumer chain completes the CCV handshake, send the reward pool address of the consumer in the handshake ACK packet + - In enblock: + - for each consumer reward pool + - filter fees using denoms whitelist + - send to distribution module account + - compute community and validators rewards + - allocate tokens to community pool and tokens + +- Consumer app runtime (unchanged): + - After distribution period each block transfers fees collected to consumer reward pool + + + +## Questions: + +- Should the consumer module accounts rather be stored as a code constant or a states? + +- Reward "pool" Naming conventions: + + - In the SDK, the community pool refers to a FeePool type struct that tracks the validators rewards. + The distribution module account holds the tokens and tracks the validator rewards using different states. + - In PSS, the consumer reward are received through an IBC transfers. That entails that it's hard to define from which consumer sent the reward. + - How can we determine for how long a validator opted in? + - Ask SDK the security diff --git a/tests/integration/distribution.go b/tests/integration/distribution.go index 25cbcb3132..e759211e16 100644 --- a/tests/integration/distribution.go +++ b/tests/integration/distribution.go @@ -1,8 +1,10 @@ package integration import ( + "fmt" "strings" + abci "github.com/cometbft/cometbft/abci/types" transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" sdk "github.com/cosmos/cosmos-sdk/types" @@ -11,7 +13,6 @@ import ( icstestingutils "github.com/cosmos/interchain-security/v4/testutil/integration" consumerkeeper "github.com/cosmos/interchain-security/v4/x/ccv/consumer/keeper" consumertypes "github.com/cosmos/interchain-security/v4/x/ccv/consumer/types" - providertypes "github.com/cosmos/interchain-security/v4/x/ccv/provider/types" ccv "github.com/cosmos/interchain-security/v4/x/ccv/types" ) @@ -39,7 +40,6 @@ func (s *CCVTestSuite) TestRewardsDistribution() { s.consumerChain.NextBlock() consumerAccountKeeper := s.consumerApp.GetTestAccountKeeper() - providerAccountKeeper := s.providerApp.GetTestAccountKeeper() consumerBankKeeper := s.consumerApp.GetTestBankKeeper() providerBankKeeper := s.providerApp.GetTestBankKeeper() @@ -79,8 +79,8 @@ func (s *CCVTestSuite) TestRewardsDistribution() { s.providerChain.NextBlock() // Since consumer reward denom is not yet registered, the coins never get into the fee pool, staying in the ConsumerRewardsPool - rewardPool := providerAccountKeeper.GetModuleAccount(s.providerCtx(), providertypes.ConsumerRewardsPool).GetAddress() - rewardCoins := providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool) + rewardPoolAddrs := s.providerApp.GetProviderKeeper().GetConsumerModuleAccountAddress(s.providerCtx(), s.consumerChain.ChainID) + rewardCoins := providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPoolAddrs) ibcCoinIndex := -1 for i, coin := range rewardCoins { @@ -97,7 +97,7 @@ func (s *CCVTestSuite) TestRewardsDistribution() { // Advance a block and check that the coins are still in the ConsumerRewardsPool s.providerChain.NextBlock() - rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool) + rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPoolAddrs) s.Require().True(rewardCoins[ibcCoinIndex].Amount.Equal(providerExpectedRewards[0].Amount)) // Set the consumer reward denom. This would be done by a governance proposal in prod @@ -106,7 +106,7 @@ func (s *CCVTestSuite) TestRewardsDistribution() { s.providerChain.NextBlock() // Check that the reward pool has no more coins because they were transferred to the fee pool - rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool) + rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPoolAddrs) s.Require().Equal(0, len(rewardCoins)) // check that the fee pool has the expected amount of coins @@ -485,3 +485,300 @@ func (s *CCVTestSuite) prepareRewardDist() { blocksToGo := bpdt - blocksSinceLastTrans s.coordinator.CommitNBlocks(s.consumerChain, uint64(blocksToGo)) } + +// This test is valid for minimal viable consumer chain +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() + acountKeeper := s.providerApp.GetTestAccountKeeper() + bankKeeper := s.providerApp.GetTestBankKeeper() + distributionKeeper := s.providerApp.GetTestDistributionKeeper() + + // consumerkeeper := s.consumerApp.GetConsumerKeeper() + chainRewardPools := map[string]authtypes.AccountI{} + for chainID, chain := range s.consumerBundles { + accName := providerKeeper.GetConsumerModuleAccount(s.providerCtx(), chainID) + acc := acountKeeper.GetModuleAccount(s.providerCtx(), accName) + // verify consumer gets the expected module account address + // after the CCV handshake + s.Require().Equal( + acc.GetAddress().String(), + chain.App.GetConsumerKeeper().GetProviderFeePoolAddrStr(chain.GetCtx()), + ) + + chainRewardPools[accName] = acc + } + + providerKeeper.SetConsumerRewardDenom(s.providerCtx(), sdk.DefaultBondDenom) // register a consumer reward denom + + // TEST BEGINBLOCKRD + delAddr := s.providerChain.SenderAccount.GetAddress() + baseFee := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))) + + // fund consumer reward pools + for rp := range chainRewardPools { + bankKeeper.SendCoinsFromAccountToModule( + s.providerCtx(), + delAddr, + rp, + baseFee, + ) + } + + // Create vote since the ibctesting NextBlock() doesn't + // implement abci votes + + 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, + }, + ) + + valReward := distributionKeeper.GetValidatorOutstandingRewards(s.providerCtx(), sdk.ValAddress(val.Address)) + fmt.Println("val outstanding reward: ", valReward.String()) + } + + for _, acc := range chainRewardPools { + bal := bankKeeper.GetAllBalances( + s.providerCtx(), + acc.GetAddress(), + ) + fmt.Println("balance :", bal) + } + + fmt.Println("community pool balance: ", distributionKeeper.GetFeePoolCommunityCoins(s.providerCtx())) + + fmt.Println("-----------Provider BeginBlock--------------") + + providerKeeper.BeginBlockRD( + s.providerCtx(), + abci.RequestBeginBlock{ + LastCommitInfo: abci.CommitInfo{ + Votes: votes, + }, + }, + ) + + for _, acc := range chainRewardPools { + bal := bankKeeper.GetAllBalances( + s.providerCtx(), + acc.GetAddress(), + ) + fmt.Println("balance: ", bal) + } + + for _, val := range s.providerChain.Vals.Validators { + + valReward := distributionKeeper.GetValidatorOutstandingRewards(s.providerCtx(), sdk.ValAddress(val.Address)) + fmt.Println("val outstanding reward: ", valReward.String()) + } + + fmt.Println("community pool balance: ", distributionKeeper.GetFeePoolCommunityCoins(s.providerCtx())) + + // // // register a consumer reward denom + // // params := s.consumerApp.GetConsumerKeeper().GetConsumerParams(s.consumerCtx()) + // // params.RewardDenoms = []string{sdk.DefaultBondDenom} + // // s.consumerApp.GetConsumerKeeper().SetParams(s.consumerCtx(), params) + + // // set module account for consumer chain 1 + // providerKeeper := s.providerApp.GetProviderKeeper() + // // acountKeeper := s.providerApp.GetTestAccountKeeper() + // bankKeeper := s.providerApp.GetTestBankKeeper() + + // // consuModAcct0 := acountKeeper.GetModuleAccount(s.providerCtx(), types.ConsumerRewardsPool0) + // // consuModAcct1 := acountKeeper.GetModuleAccount(s.providerCtx(), types.ConsumerRewardsPool1) + // // consuModAcct2 := acountKeeper.GetModuleAccount(s.providerCtx(), types.ConsumerRewardsPool2) + + // consuModAcctNames := []string{ + // // types.ConsumerRewardsPool0, + // // types.ConsumerRewardsPool1, + // // types.ConsumerRewardsPool2, + // } + + // baseFee := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))) + // delAddr := s.providerChain.SenderAccount.GetAddress() + // validators := s.providerChain.Vals.Validators + + // providerKeeper.SetConsumerRewardDenom(s.providerCtx(), sdk.DefaultBondDenom) // // register a consumer reward denom + // // params := s.consumerApp.GetConsumerKeeper().GetConsumerParams(s.consumerCtx()) + // // params.RewardDenoms = []string{sdk.DefaultBondDenom} + // // s.consumerApp.GetConsumerKeeper().SetParams(s.consumerCtx(), params)) + + // for i := 0; i < 3; i++ { + // fee := baseFee.Add(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(int64(i)))) + + // bankKeeper.SendCoinsFromAccountToModule( + // s.providerCtx(), + // delAddr, + // consuModAcctNames[i], + // fee, + // ) + + // s.Require().True( + // fee.IsEqual( + // bankKeeper.GetAllBalances( + // s.providerCtx(), + // authtypes.NewModuleAddress(consuModAcctNames[i]), + // ), + // ), + // ) + + // // first consumer has all the validators optIn already (preProposalKeyAssignment) + // if i > 0 { + // val := validators[i] + // chainID := s.consumerBundles["testchain"+strconv.Itoa(i+2)].Chain.ChainID // DIRTY + // // get SDK validator + // valAddr, err := sdk.ValAddressFromHex(val.Address.String()) + // s.Require().NoError(err) + // validator := s.getVal(s.providerCtx(), valAddr) + + // // generate new PrivValidator + // privVal := mock.NewPV() + // tmPubKey, err := privVal.GetPubKey() + // s.Require().NoError(err) + // consumerKey, err := tmencoding.PubKeyToProto(tmPubKey) + // s.Require().NoError(err) + + // // add Signer to the provider chain as there is no consumer chain to add it; + // // as a result, NewTestChainWithValSet in AddConsumer uses providerChain.Signers + // s.providerChain.Signers[tmPubKey.Address().String()] = privVal + + // err = providerKeeper.AssignConsumerKey(s.providerCtx(), chainID, validator, consumerKey) + // s.Require().NoError(err) + // } + + // valOldBal := map[string]sdk.DecCoins{} + // votes := []abci.VoteInfo{} + + // for _, val := range validators { + // valAddr, err := sdk.ValAddressFromHex(val.Address.String()) + // s.Require().NoError(err) + // valOldBal[valAddr.String()] = s.providerApp.GetTestDistributionKeeper().GetValidatorOutstandingRewards(s.providerCtx(), valAddr).Rewards + // votes = append(votes, + // abci.VoteInfo{ + // Validator: abci.Validator{Address: val.Address, Power: val.VotingPower}, + // SignedLastBlock: true, + // }, + // ) + // } + + // providerKeeper.AllocateTokens(s.providerCtx(), s.providerChain.Vals.TotalVotingPower(), votes) + + // // s.providerChain.LastHeader.ValidatorSet.TotalVotingPower + + // for _, val := range validators { + // valAddr, err := sdk.ValAddressFromHex(val.Address.String()) + // s.Require().NoError(err) + // valOldBal[valAddr.String()] = s.providerApp.GetTestDistributionKeeper().GetValidatorOutstandingRewards(s.providerCtx(), valAddr).Rewards + // } + + // } + + // fmt.Println(providerKeeper.GetAllValidatorConsumerPubKeys(s.providerCtx(), &s.consumerChain.ChainID)) + // fmt.Println(providerKeeper.GetAllValidatorConsumerPubKeys(s.providerCtx(), &(s.consumerBundles["testchain3"].Chain.ChainID))) + // fmt.Println(providerKeeper.GetAllValidatorConsumerPubKeys(s.providerCtx(), &(s.consumerBundles["testchain4"].Chain.ChainID))) + + // ak := s.providerApp.GetTestAccountKeeper() + // ak.GetModuleAccount(ctx) + // ak := s.providerApp.GetTestAccountKeeper().SetModuleAccount(s.providerCtx(), consuModAcct) + + // fees := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))) + // err := s.providerApp.GetTestBankKeeper().SendCoinsFromAccountToModule(s.providerCtx(), delAddr, moduleName, fees) + + // s.Require().NoError(err) + + // delegate(s, delAddr, bondAmt) + // s.providerChain.NextBlock() + + // // register a consumer reward denom + // params := s.consumerApp.GetConsumerKeeper().GetConsumerParams(s.consumerCtx()) + // params.RewardDenoms = []string{sdk.DefaultBondDenom} + // s.consumerApp.GetConsumerKeeper().SetParams(s.consumerCtx(), params) + + // // relay VSC packets from provider to consumer + // relayAllCommittedPackets(s, s.providerChain, s.path, ccv.ProviderPortID, s.path.EndpointB.ChannelID, 1) + + // // reward for the provider chain will be sent after each 2 blocks + // consumerParams := s.consumerApp.GetSubspace(consumertypes.ModuleName) + // consumerParams.Set(s.consumerCtx(), ccv.KeyBlocksPerDistributionTransmission, int64(2)) + // s.consumerChain.NextBlock() + + // consumerAccountKeeper := s.consumerApp.GetTestAccountKeeper() + // providerAccountKeeper := s.providerApp.GetTestAccountKeeper() + // consumerBankKeeper := s.consumerApp.GetTestBankKeeper() + // providerBankKeeper := s.providerApp.GetTestBankKeeper() + + // // send coins to the fee pool which is used for reward distribution + // consumerFeePoolAddr := consumerAccountKeeper.GetModuleAccount(s.consumerCtx(), authtypes.FeeCollectorName).GetAddress() + // feePoolTokensOld := consumerBankKeeper.GetAllBalances(s.consumerCtx(), consumerFeePoolAddr) + // fees := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100))) + // err := consumerBankKeeper.SendCoinsFromAccountToModule(s.consumerCtx(), s.consumerChain.SenderAccount.GetAddress(), authtypes.FeeCollectorName, fees) + // s.Require().NoError(err) + // feePoolTokens := consumerBankKeeper.GetAllBalances(s.consumerCtx(), consumerFeePoolAddr) + // s.Require().Equal(sdk.NewInt(100).Add(feePoolTokensOld.AmountOf(sdk.DefaultBondDenom)), feePoolTokens.AmountOf(sdk.DefaultBondDenom)) + + // // calculate the reward for consumer and provider chain. Consumer will receive ConsumerRedistributeFrac, the rest is going to provider + // frac, err := sdk.NewDecFromStr(s.consumerApp.GetConsumerKeeper().GetConsumerRedistributionFrac(s.consumerCtx())) + // s.Require().NoError(err) + // consumerExpectedRewards, _ := sdk.NewDecCoinsFromCoins(feePoolTokens...).MulDec(frac).TruncateDecimal() + // providerExpectedRewards := feePoolTokens.Sub(consumerExpectedRewards...) + // s.consumerChain.NextBlock() + + // // amount from the fee pool is divided between consumer redistribute address and address reserved for provider chain + // feePoolTokens = consumerBankKeeper.GetAllBalances(s.consumerCtx(), consumerFeePoolAddr) + // s.Require().Equal(0, len(feePoolTokens)) + // consumerRedistributeAddr := consumerAccountKeeper.GetModuleAccount(s.consumerCtx(), consumertypes.ConsumerRedistributeName).GetAddress() + // consumerTokens := consumerBankKeeper.GetAllBalances(s.consumerCtx(), consumerRedistributeAddr) + // s.Require().Equal(consumerExpectedRewards.AmountOf(sdk.DefaultBondDenom), consumerTokens.AmountOf(sdk.DefaultBondDenom)) + // providerRedistributeAddr := consumerAccountKeeper.GetModuleAccount(s.consumerCtx(), consumertypes.ConsumerToSendToProviderName).GetAddress() + // providerTokens := consumerBankKeeper.GetAllBalances(s.consumerCtx(), providerRedistributeAddr) + // s.Require().Equal(providerExpectedRewards.AmountOf(sdk.DefaultBondDenom), providerTokens.AmountOf(sdk.DefaultBondDenom)) + + // // send the reward to provider chain after 2 blocks + + // s.consumerChain.NextBlock() + // providerTokens = consumerBankKeeper.GetAllBalances(s.consumerCtx(), providerRedistributeAddr) + // s.Require().Equal(0, len(providerTokens)) + + // relayAllCommittedPackets(s, s.consumerChain, s.transferPath, transfertypes.PortID, s.transferPath.EndpointA.ChannelID, 1) + // s.providerChain.NextBlock() + + // // Since consumer reward denom is not yet registered, the coins never get into the fee pool, staying in the ConsumerRewardsPool + // rewardPool := providerAccountKeeper.GetModuleAccount(s.providerCtx(), providertypes.ConsumerRewardsPool).GetAddress() + // rewardCoins := providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool) + + // ibcCoinIndex := -1 + // for i, coin := range rewardCoins { + // if strings.HasPrefix(coin.Denom, "ibc") { + // ibcCoinIndex = i + // } + // } + + // // Check that we found an ibc denom in the reward pool + // s.Require().Greater(ibcCoinIndex, -1) + + // // Check that the coins got into the ConsumerRewardsPool + // s.Require().True(rewardCoins[ibcCoinIndex].Amount.Equal(providerExpectedRewards[0].Amount)) + + // // Advance a block and check that the coins are still in the ConsumerRewardsPool + // s.providerChain.NextBlock() + // rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool) + // s.Require().True(rewardCoins[ibcCoinIndex].Amount.Equal(providerExpectedRewards[0].Amount)) + + // // Set the consumer reward denom. This would be done by a governance proposal in prod + // s.providerApp.GetProviderKeeper().SetConsumerRewardDenom(s.providerCtx(), rewardCoins[ibcCoinIndex].Denom) + + // s.providerChain.NextBlock() + + // // Check that the reward pool has no more coins because they were transferred to the fee pool + // rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool) + // s.Require().Equal(0, len(rewardCoins)) + + // // check that the fee pool has the expected amount of coins + // communityCoins := s.providerApp.GetTestDistributionKeeper().GetFeePoolCommunityCoins(s.providerCtx()) + // s.Require().True(communityCoins[ibcCoinIndex].Amount.Equal(sdk.NewDecCoinFromCoin(providerExpectedRewards[0]).Amount)) +} diff --git a/testutil/ibc_testing/generic_setup.go b/testutil/ibc_testing/generic_setup.go index 6d17337853..0ed23d5a9b 100644 --- a/testutil/ibc_testing/generic_setup.go +++ b/testutil/ibc_testing/generic_setup.go @@ -135,6 +135,9 @@ func AddConsumer[Tp testutil.ProviderApp, Tc testutil.ConsumerApp]( ) s.Require().NoError(err) + // assign a module account to consumer + providerKeeper.AssignNextRewardPoolToConsumer(providerChain.GetContext(), prop.ChainId) + // commit the state on the provider chain coordinator.CommitBlock(providerChain) diff --git a/testutil/integration/debug_test.go b/testutil/integration/debug_test.go index 77d460f05f..bda6e06fd3 100644 --- a/testutil/integration/debug_test.go +++ b/testutil/integration/debug_test.go @@ -77,6 +77,10 @@ func TestSendRewardsRetries(t *testing.T) { runCCVTestByName(t, "TestSendRewardsRetries") } +func TestAllocateTokens(t *testing.T) { + runCCVTestByName(t, "TestAllocateTokens") +} + func TestRewardsDistribution(t *testing.T) { runCCVTestByName(t, "TestRewardsDistribution") } diff --git a/testutil/integration/interfaces.go b/testutil/integration/interfaces.go index 89d59904df..550d3864ad 100644 --- a/testutil/integration/interfaces.go +++ b/testutil/integration/interfaces.go @@ -119,6 +119,7 @@ type TestBankKeeper interface { type TestAccountKeeper interface { ccvtypes.AccountKeeper GetParams(sdk.Context) authtypes.Params + SetModuleAccount(sdk.Context, authtypes.ModuleAccountI) } type TestSlashingKeeper interface { diff --git a/testutil/keeper/mocks.go b/testutil/keeper/mocks.go index 5f9d9b2694..e588f51e30 100644 --- a/testutil/keeper/mocks.go +++ b/testutil/keeper/mocks.go @@ -14,13 +14,14 @@ import ( types0 "github.com/cosmos/cosmos-sdk/types" types1 "github.com/cosmos/cosmos-sdk/x/auth/types" types2 "github.com/cosmos/cosmos-sdk/x/capability/types" + types3 "github.com/cosmos/cosmos-sdk/x/distribution/types" v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" - types3 "github.com/cosmos/cosmos-sdk/x/slashing/types" - types4 "github.com/cosmos/cosmos-sdk/x/staking/types" - types5 "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" - types6 "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" - types7 "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" - types8 "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + types4 "github.com/cosmos/cosmos-sdk/x/slashing/types" + types5 "github.com/cosmos/cosmos-sdk/x/staking/types" + types6 "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + types7 "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + types8 "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" + types9 "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" exported "github.com/cosmos/ibc-go/v7/modules/core/exported" gomock "github.com/golang/mock/gomock" ) @@ -63,10 +64,10 @@ func (mr *MockStakingKeeperMockRecorder) BondDenom(ctx interface{}) *gomock.Call } // Delegation mocks base method. -func (m *MockStakingKeeper) Delegation(ctx types0.Context, addr types0.AccAddress, valAddr types0.ValAddress) types4.DelegationI { +func (m *MockStakingKeeper) Delegation(ctx types0.Context, addr types0.AccAddress, valAddr types0.ValAddress) types5.DelegationI { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Delegation", ctx, addr, valAddr) - ret0, _ := ret[0].(types4.DelegationI) + ret0, _ := ret[0].(types5.DelegationI) return ret0 } @@ -105,10 +106,10 @@ func (mr *MockStakingKeeperMockRecorder) GetLastValidatorPower(ctx, operator int } // GetLastValidators mocks base method. -func (m *MockStakingKeeper) GetLastValidators(ctx types0.Context) []types4.Validator { +func (m *MockStakingKeeper) GetLastValidators(ctx types0.Context) []types5.Validator { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetLastValidators", ctx) - ret0, _ := ret[0].([]types4.Validator) + ret0, _ := ret[0].([]types5.Validator) return ret0 } @@ -119,10 +120,10 @@ func (mr *MockStakingKeeperMockRecorder) GetLastValidators(ctx interface{}) *gom } // GetRedelegationsFromSrcValidator mocks base method. -func (m *MockStakingKeeper) GetRedelegationsFromSrcValidator(ctx types0.Context, valAddr types0.ValAddress) []types4.Redelegation { +func (m *MockStakingKeeper) GetRedelegationsFromSrcValidator(ctx types0.Context, valAddr types0.ValAddress) []types5.Redelegation { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRedelegationsFromSrcValidator", ctx, valAddr) - ret0, _ := ret[0].([]types4.Redelegation) + ret0, _ := ret[0].([]types5.Redelegation) return ret0 } @@ -133,10 +134,10 @@ func (mr *MockStakingKeeperMockRecorder) GetRedelegationsFromSrcValidator(ctx, v } // GetUnbondingDelegationsFromValidator mocks base method. -func (m *MockStakingKeeper) GetUnbondingDelegationsFromValidator(ctx types0.Context, valAddr types0.ValAddress) []types4.UnbondingDelegation { +func (m *MockStakingKeeper) GetUnbondingDelegationsFromValidator(ctx types0.Context, valAddr types0.ValAddress) []types5.UnbondingDelegation { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetUnbondingDelegationsFromValidator", ctx, valAddr) - ret0, _ := ret[0].([]types4.UnbondingDelegation) + ret0, _ := ret[0].([]types5.UnbondingDelegation) return ret0 } @@ -147,10 +148,10 @@ func (mr *MockStakingKeeperMockRecorder) GetUnbondingDelegationsFromValidator(ct } // GetUnbondingType mocks base method. -func (m *MockStakingKeeper) GetUnbondingType(ctx types0.Context, id uint64) (types4.UnbondingType, bool) { +func (m *MockStakingKeeper) GetUnbondingType(ctx types0.Context, id uint64) (types5.UnbondingType, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetUnbondingType", ctx, id) - ret0, _ := ret[0].(types4.UnbondingType) + ret0, _ := ret[0].(types5.UnbondingType) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -162,10 +163,10 @@ func (mr *MockStakingKeeperMockRecorder) GetUnbondingType(ctx, id interface{}) * } // GetValidator mocks base method. -func (m *MockStakingKeeper) GetValidator(ctx types0.Context, addr types0.ValAddress) (types4.Validator, bool) { +func (m *MockStakingKeeper) GetValidator(ctx types0.Context, addr types0.ValAddress) (types5.Validator, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetValidator", ctx, addr) - ret0, _ := ret[0].(types4.Validator) + ret0, _ := ret[0].(types5.Validator) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -177,10 +178,10 @@ func (mr *MockStakingKeeperMockRecorder) GetValidator(ctx, addr interface{}) *go } // GetValidatorByConsAddr mocks base method. -func (m *MockStakingKeeper) GetValidatorByConsAddr(ctx types0.Context, consAddr types0.ConsAddress) (types4.Validator, bool) { +func (m *MockStakingKeeper) GetValidatorByConsAddr(ctx types0.Context, consAddr types0.ConsAddress) (types5.Validator, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetValidatorByConsAddr", ctx, consAddr) - ret0, _ := ret[0].(types4.Validator) + ret0, _ := ret[0].(types5.Validator) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -232,7 +233,7 @@ func (mr *MockStakingKeeperMockRecorder) IterateLastValidatorPowers(ctx, cb inte } // IterateValidators mocks base method. -func (m *MockStakingKeeper) IterateValidators(ctx types0.Context, f func(int64, types4.ValidatorI) bool) { +func (m *MockStakingKeeper) IterateValidators(ctx types0.Context, f func(int64, types5.ValidatorI) bool) { m.ctrl.T.Helper() m.ctrl.Call(m, "IterateValidators", ctx, f) } @@ -312,7 +313,7 @@ func (mr *MockStakingKeeperMockRecorder) Slash(arg0, arg1, arg2, arg3, arg4 inte } // SlashRedelegation mocks base method. -func (m *MockStakingKeeper) SlashRedelegation(arg0 types0.Context, arg1 types4.Validator, arg2 types4.Redelegation, arg3 int64, arg4 types0.Dec) math.Int { +func (m *MockStakingKeeper) SlashRedelegation(arg0 types0.Context, arg1 types5.Validator, arg2 types5.Redelegation, arg3 int64, arg4 types0.Dec) math.Int { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SlashRedelegation", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(math.Int) @@ -326,7 +327,7 @@ func (mr *MockStakingKeeperMockRecorder) SlashRedelegation(arg0, arg1, arg2, arg } // SlashUnbondingDelegation mocks base method. -func (m *MockStakingKeeper) SlashUnbondingDelegation(arg0 types0.Context, arg1 types4.UnbondingDelegation, arg2 int64, arg3 types0.Dec) math.Int { +func (m *MockStakingKeeper) SlashUnbondingDelegation(arg0 types0.Context, arg1 types5.UnbondingDelegation, arg2 int64, arg3 types0.Dec) math.Int { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SlashUnbondingDelegation", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(math.Int) @@ -340,7 +341,7 @@ func (mr *MockStakingKeeperMockRecorder) SlashUnbondingDelegation(arg0, arg1, ar } // SlashWithInfractionReason mocks base method. -func (m *MockStakingKeeper) SlashWithInfractionReason(arg0 types0.Context, arg1 types0.ConsAddress, arg2, arg3 int64, arg4 types0.Dec, arg5 types4.Infraction) math.Int { +func (m *MockStakingKeeper) SlashWithInfractionReason(arg0 types0.Context, arg1 types0.ConsAddress, arg2, arg3 int64, arg4 types0.Dec, arg5 types5.Infraction) math.Int { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SlashWithInfractionReason", arg0, arg1, arg2, arg3, arg4, arg5) ret0, _ := ret[0].(math.Int) @@ -394,10 +395,10 @@ func (mr *MockStakingKeeperMockRecorder) Unjail(ctx, addr interface{}) *gomock.C } // Validator mocks base method. -func (m *MockStakingKeeper) Validator(ctx types0.Context, addr types0.ValAddress) types4.ValidatorI { +func (m *MockStakingKeeper) Validator(ctx types0.Context, addr types0.ValAddress) types5.ValidatorI { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Validator", ctx, addr) - ret0, _ := ret[0].(types4.ValidatorI) + ret0, _ := ret[0].(types5.ValidatorI) return ret0 } @@ -408,10 +409,10 @@ func (mr *MockStakingKeeperMockRecorder) Validator(ctx, addr interface{}) *gomoc } // ValidatorByConsAddr mocks base method. -func (m *MockStakingKeeper) ValidatorByConsAddr(ctx types0.Context, consAddr types0.ConsAddress) types4.ValidatorI { +func (m *MockStakingKeeper) ValidatorByConsAddr(ctx types0.Context, consAddr types0.ConsAddress) types5.ValidatorI { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidatorByConsAddr", ctx, consAddr) - ret0, _ := ret[0].(types4.ValidatorI) + ret0, _ := ret[0].(types5.ValidatorI) return ret0 } @@ -459,10 +460,10 @@ func (mr *MockSlashingKeeperMockRecorder) DowntimeJailDuration(arg0 interface{}) } // GetValidatorSigningInfo mocks base method. -func (m *MockSlashingKeeper) GetValidatorSigningInfo(ctx types0.Context, address types0.ConsAddress) (types3.ValidatorSigningInfo, bool) { +func (m *MockSlashingKeeper) GetValidatorSigningInfo(ctx types0.Context, address types0.ConsAddress) (types4.ValidatorSigningInfo, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetValidatorSigningInfo", ctx, address) - ret0, _ := ret[0].(types3.ValidatorSigningInfo) + ret0, _ := ret[0].(types4.ValidatorSigningInfo) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -473,18 +474,6 @@ func (mr *MockSlashingKeeperMockRecorder) GetValidatorSigningInfo(ctx, address i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValidatorSigningInfo", reflect.TypeOf((*MockSlashingKeeper)(nil).GetValidatorSigningInfo), ctx, address) } -// SetValidatorSigningInfo mocks base method. -func (m *MockSlashingKeeper) SetValidatorSigningInfo(ctx types0.Context, address types0.ConsAddress, info types3.ValidatorSigningInfo) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetValidatorSigningInfo", ctx, address, info) -} - -// SetValidatorSigningInfo indicates an expected call of SetValidatorSigningInfo. -func (mr *MockSlashingKeeperMockRecorder) SetValidatorSigningInfo(ctx, address interface{}, info interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetValidatorSigningInfo", reflect.TypeOf((*MockSlashingKeeper)(nil).SetValidatorSigningInfo), ctx, address, info) -} - // IsTombstoned mocks base method. func (m *MockSlashingKeeper) IsTombstoned(arg0 types0.Context, arg1 types0.ConsAddress) bool { m.ctrl.T.Helper() @@ -511,6 +500,18 @@ func (mr *MockSlashingKeeperMockRecorder) JailUntil(arg0, arg1, arg2 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JailUntil", reflect.TypeOf((*MockSlashingKeeper)(nil).JailUntil), arg0, arg1, arg2) } +// SetValidatorSigningInfo mocks base method. +func (m *MockSlashingKeeper) SetValidatorSigningInfo(ctx types0.Context, address types0.ConsAddress, info types4.ValidatorSigningInfo) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetValidatorSigningInfo", ctx, address, info) +} + +// SetValidatorSigningInfo indicates an expected call of SetValidatorSigningInfo. +func (mr *MockSlashingKeeperMockRecorder) SetValidatorSigningInfo(ctx, address, info interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetValidatorSigningInfo", reflect.TypeOf((*MockSlashingKeeper)(nil).SetValidatorSigningInfo), ctx, address, info) +} + // SlashFractionDoubleSign mocks base method. func (m *MockSlashingKeeper) SlashFractionDoubleSign(ctx types0.Context) types0.Dec { m.ctrl.T.Helper() @@ -589,10 +590,10 @@ func (mr *MockChannelKeeperMockRecorder) ChanCloseInit(ctx, portID, channelID, c } // GetChannel mocks base method. -func (m *MockChannelKeeper) GetChannel(ctx types0.Context, srcPort, srcChan string) (types8.Channel, bool) { +func (m *MockChannelKeeper) GetChannel(ctx types0.Context, srcPort, srcChan string) (types9.Channel, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetChannel", ctx, srcPort, srcChan) - ret0, _ := ret[0].(types8.Channel) + ret0, _ := ret[0].(types9.Channel) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -635,7 +636,7 @@ func (mr *MockChannelKeeperMockRecorder) GetNextSequenceSend(ctx, portID, channe } // SendPacket mocks base method. -func (m *MockChannelKeeper) SendPacket(ctx types0.Context, chanCap *types2.Capability, sourcePort, sourceChannel string, timeoutHeight types6.Height, timeoutTimestamp uint64, data []byte) (uint64, error) { +func (m *MockChannelKeeper) SendPacket(ctx types0.Context, chanCap *types2.Capability, sourcePort, sourceChannel string, timeoutHeight types7.Height, timeoutTimestamp uint64, data []byte) (uint64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendPacket", ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) ret0, _ := ret[0].(uint64) @@ -724,10 +725,10 @@ func (m *MockConnectionKeeper) EXPECT() *MockConnectionKeeperMockRecorder { } // GetConnection mocks base method. -func (m *MockConnectionKeeper) GetConnection(ctx types0.Context, connectionID string) (types7.ConnectionEnd, bool) { +func (m *MockConnectionKeeper) GetConnection(ctx types0.Context, connectionID string) (types8.ConnectionEnd, bool) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetConnection", ctx, connectionID) - ret0, _ := ret[0].(types7.ConnectionEnd) + ret0, _ := ret[0].(types8.ConnectionEnd) ret1, _ := ret[1].(bool) return ret0, ret1 } @@ -885,6 +886,18 @@ func (m *MockDistributionKeeper) EXPECT() *MockDistributionKeeperMockRecorder { return m.recorder } +// AllocateTokensToValidator mocks base method. +func (m *MockDistributionKeeper) AllocateTokensToValidator(ctx types0.Context, validator types5.ValidatorI, reward types0.DecCoins) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AllocateTokensToValidator", ctx, validator, reward) +} + +// AllocateTokensToValidator indicates an expected call of AllocateTokensToValidator. +func (mr *MockDistributionKeeperMockRecorder) AllocateTokensToValidator(ctx, validator, reward interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllocateTokensToValidator", reflect.TypeOf((*MockDistributionKeeper)(nil).AllocateTokensToValidator), ctx, validator, reward) +} + // FundCommunityPool mocks base method. func (m *MockDistributionKeeper) FundCommunityPool(ctx types0.Context, amount types0.Coins, sender types0.AccAddress) error { m.ctrl.T.Helper() @@ -899,6 +912,46 @@ func (mr *MockDistributionKeeperMockRecorder) FundCommunityPool(ctx, amount, sen return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FundCommunityPool", reflect.TypeOf((*MockDistributionKeeper)(nil).FundCommunityPool), ctx, amount, sender) } +// GetCommunityTax mocks base method. +func (m *MockDistributionKeeper) GetCommunityTax(ctx types0.Context) math.LegacyDec { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommunityTax", ctx) + ret0, _ := ret[0].(math.LegacyDec) + return ret0 +} + +// GetCommunityTax indicates an expected call of GetCommunityTax. +func (mr *MockDistributionKeeperMockRecorder) GetCommunityTax(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommunityTax", reflect.TypeOf((*MockDistributionKeeper)(nil).GetCommunityTax), ctx) +} + +// GetFeePool mocks base method. +func (m *MockDistributionKeeper) GetFeePool(ctx types0.Context) types3.FeePool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeePool", ctx) + ret0, _ := ret[0].(types3.FeePool) + return ret0 +} + +// GetFeePool indicates an expected call of GetFeePool. +func (mr *MockDistributionKeeperMockRecorder) GetFeePool(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeePool", reflect.TypeOf((*MockDistributionKeeper)(nil).GetFeePool), ctx) +} + +// SetFeePool mocks base method. +func (m *MockDistributionKeeper) SetFeePool(ctx types0.Context, feePool types3.FeePool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetFeePool", ctx, feePool) +} + +// SetFeePool indicates an expected call of SetFeePool. +func (mr *MockDistributionKeeperMockRecorder) SetFeePool(ctx, feePool interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFeePool", reflect.TypeOf((*MockDistributionKeeper)(nil).SetFeePool), ctx, feePool) +} + // MockConsumerHooks is a mock of ConsumerHooks interface. type MockConsumerHooks struct { ctrl *gomock.Controller @@ -1062,10 +1115,10 @@ func (m *MockIBCTransferKeeper) EXPECT() *MockIBCTransferKeeperMockRecorder { } // Transfer mocks base method. -func (m *MockIBCTransferKeeper) Transfer(arg0 context.Context, arg1 *types5.MsgTransfer) (*types5.MsgTransferResponse, error) { +func (m *MockIBCTransferKeeper) Transfer(arg0 context.Context, arg1 *types6.MsgTransfer) (*types6.MsgTransferResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Transfer", arg0, arg1) - ret0, _ := ret[0].(*types5.MsgTransferResponse) + ret0, _ := ret[0].(*types6.MsgTransferResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1100,10 +1153,10 @@ func (m *MockIBCCoreKeeper) EXPECT() *MockIBCCoreKeeperMockRecorder { } // ChannelOpenInit mocks base method. -func (m *MockIBCCoreKeeper) ChannelOpenInit(goCtx context.Context, msg *types8.MsgChannelOpenInit) (*types8.MsgChannelOpenInitResponse, error) { +func (m *MockIBCCoreKeeper) ChannelOpenInit(goCtx context.Context, msg *types9.MsgChannelOpenInit) (*types9.MsgChannelOpenInitResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ChannelOpenInit", goCtx, msg) - ret0, _ := ret[0].(*types8.MsgChannelOpenInitResponse) + ret0, _ := ret[0].(*types9.MsgChannelOpenInitResponse) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/x/ccv/provider/ibc_module.go b/x/ccv/provider/ibc_module.go index 3b4f0fa050..ded217907a 100644 --- a/x/ccv/provider/ibc_module.go +++ b/x/ccv/provider/ibc_module.go @@ -78,9 +78,10 @@ func (am AppModule) OnChanOpenTry( return "", err } - if err := am.keeper.VerifyConsumerChain( + chainID, err := am.keeper.VerifyConsumerChain( ctx, channelID, connectionHops, - ); err != nil { + ) + if err != nil { return "", err } @@ -89,8 +90,11 @@ func (am AppModule) OnChanOpenTry( // the consumer chain must be excluded from the blocked addresses // blacklist or all all ibc-transfers from the consumer chain to the // provider chain will fail - ProviderFeePoolAddr: am.keeper.GetConsumerRewardsPoolAddressStr(ctx), - Version: ccv.Version, + ProviderFeePoolAddr: am.keeper.GetConsumerModuleAccountAddress( + ctx, + chainID, + ).String(), + Version: ccv.Version, } mdBz, err := (&md).Marshal() if err != nil { diff --git a/x/ccv/provider/ibc_module_test.go b/x/ccv/provider/ibc_module_test.go index df12ed4cb8..fc53ce98b5 100644 --- a/x/ccv/provider/ibc_module_test.go +++ b/x/ccv/provider/ibc_module_test.go @@ -12,13 +12,11 @@ import ( "github.com/stretchr/testify/require" sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" testkeeper "github.com/cosmos/interchain-security/v4/testutil/keeper" "github.com/cosmos/interchain-security/v4/x/ccv/provider" providerkeeper "github.com/cosmos/interchain-security/v4/x/ccv/provider/keeper" - providertypes "github.com/cosmos/interchain-security/v4/x/ccv/provider/types" ccv "github.com/cosmos/interchain-security/v4/x/ccv/types" ) @@ -137,20 +135,19 @@ func TestOnChanOpenTry(t *testing.T) { } // Expected mock calls - moduleAcct := authtypes.ModuleAccount{BaseAccount: &authtypes.BaseAccount{}} - moduleAcct.BaseAccount.Address = authtypes.NewModuleAddress(providertypes.ConsumerRewardsPool).String() + moduleAcct := providerKeeper.GetConsumerModuleAccountAddress(ctx, "consumerChainID") // Number of calls is not asserted, since not all code paths are hit for failures gomock.InOrder( mocks.MockScopedKeeper.EXPECT().ClaimCapability( - params.ctx, params.chanCap, host.ChannelCapabilityPath(params.portID, params.channelID)).AnyTimes(), + params.ctx, params.chanCap, host.ChannelCapabilityPath(params.portID, params.channelID), + ).AnyTimes(), mocks.MockConnectionKeeper.EXPECT().GetConnection(ctx, "connectionIDToConsumer").Return( conntypes.ConnectionEnd{ClientId: "clientIDToConsumer"}, true, ).AnyTimes(), mocks.MockClientKeeper.EXPECT().GetClientState(ctx, "clientIDToConsumer").Return( &ibctmtypes.ClientState{ChainId: "consumerChainID"}, true, ).AnyTimes(), - mocks.MockAccountKeeper.EXPECT().GetModuleAccount(ctx, providertypes.ConsumerRewardsPool).Return(&moduleAcct).AnyTimes(), ) tc.mutateParams(¶ms, &providerKeeper) @@ -171,7 +168,7 @@ func TestOnChanOpenTry(t *testing.T) { md := &ccv.HandshakeMetadata{} err = md.Unmarshal([]byte(metadata)) require.NoError(t, err) - require.Equal(t, moduleAcct.BaseAccount.Address, md.ProviderFeePoolAddr, + require.Equal(t, moduleAcct.String(), md.ProviderFeePoolAddr, "returned dist account metadata must match expected") require.Equal(t, ccv.Version, md.Version, "returned ccv version metadata must match expected") ctrl.Finish() diff --git a/x/ccv/provider/keeper/distribution.go b/x/ccv/provider/keeper/distribution.go index 5b2e6025ef..0287788a4b 100644 --- a/x/ccv/provider/keeper/distribution.go +++ b/x/ccv/provider/keeper/distribution.go @@ -1,22 +1,29 @@ package keeper import ( + "cosmossdk.io/math" + abci "github.com/cometbft/cometbft/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" - + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" "github.com/cosmos/interchain-security/v4/x/ccv/provider/types" ) -// EndBlockRD executes EndBlock logic for the Reward Distribution sub-protocol. -// Reward Distribution follows a simple model: send tokens to the ConsumerRewardsPool, -// from where they sent to the fee collector address -func (k Keeper) EndBlockRD(ctx sdk.Context) { +// 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 - k.TransferRewardsToFeeCollector(ctx) -} -func (k Keeper) GetConsumerRewardsPoolAddressStr(ctx sdk.Context) string { - return k.accountKeeper.GetModuleAccount( - ctx, types.ConsumerRewardsPool).GetAddress().String() + // determine the total power signing the block + var previousTotalPower int64 + for _, voteInfo := range req.LastCommitInfo.GetVotes() { + previousTotalPower += voteInfo.Validator.Power + } + + // TODO this is Tendermint-dependent + // ref https://github.com/cosmos/cosmos-sdk/issues/3095 + if ctx.BlockHeight() > 1 { + k.AllocateTokens(ctx, previousTotalPower, req.LastCommitInfo.GetVotes()) + } } func (k Keeper) SetConsumerRewardDenom( @@ -57,32 +64,142 @@ func (k Keeper) GetAllConsumerRewardDenoms(ctx sdk.Context) (consumerRewardDenom return consumerRewardDenoms } -// TransferRewardsToFeeCollector transfers all consumer rewards to the fee collector address -func (k Keeper) TransferRewardsToFeeCollector(ctx sdk.Context) { - // 1. Get the denom whitelist from the store +// ProviderAllocateConsumerRewards: + +// args (call by BeginBlock) +// - totalPreviousPower int64, bondedVotes []abci.VoteInfo + +// providers states: +// - whitelisted denoms +// - consumer chains +// - Pool per +// - validators per consumer + +// Naive Flow +// For each consumer +// - get ConsumerRewardsPool +// - iterate over balance denoms +// - if denoms is whitelisted send to validators +// - distribute tokens by iterating over validators <- power fraction per validators is always the same + +// Opti Flow +// For each pool get fee collected + +// For each bondedVotes +// compute powerFraction +// For each consumer the bondVote.Val OptIn +// AllocateTokens of consumer.filtered pool + +// total remaining + +// AllocateTokens performs reward and fee distribution to all validators based +// on the F1 fee distribution specification. +func (k Keeper) AllocateTokens(ctx sdk.Context, totalPreviousPower int64, bondedVotes []abci.VoteInfo) { + consuChains := k.GetAllConsumerChains(ctx) + + // Allocate tokens to validator for each consumer they opted in to + for _, consu := range consuChains { + + consuFeeCollected := k.GetConsumerFeeCollected(ctx, consu.ChainId) + if consuFeeCollected.IsZero() { + continue + } + + // transfer collected fees to the distribution module account + err := k.bankKeeper.SendCoinsFromModuleToModule( + ctx, + k.GetConsumerModuleAccount(ctx, consu.ChainId), + distributiontypes.ModuleName, + consuFeeCollected, + ) + if err != nil { + panic(err) + } + + feesCollected := sdk.NewDecCoinsFromCoins(consuFeeCollected...) + + // temporary workaround to keep CanWithdrawInvariant happy + // general discussions here: https://github.com/cosmos/cosmos-sdk/issues/2906#issuecomment-441867634 + feePool := k.distributionKeeper.GetFeePool(ctx) + if totalPreviousPower == 0 { + feePool.CommunityPool = feePool.CommunityPool.Add(feesCollected...) + k.distributionKeeper.SetFeePool(ctx, feePool) + return + } + + // calculate fraction allocated to validators + remaining := feesCollected + communityTax := k.distributionKeeper.GetCommunityTax(ctx) + voteMultiplier := math.LegacyOneDec().Sub(communityTax) + feeMultiplier := feesCollected.MulDecTruncate(voteMultiplier) + + // allocate tokens to consumer validators + + feesAllocated := k.AllocateTokensToConsumerValidators( + ctx, + consu.ChainId, + totalPreviousPower, + bondedVotes, + feeMultiplier, + ) + remaining = remaining.Sub(feesAllocated) + + // allocate community funding + feePool.CommunityPool = feePool.CommunityPool.Add(remaining...) + k.distributionKeeper.SetFeePool(ctx, feePool) + } +} + +// TODO: define which validators earned the tokens i.e. already opted in for 1000 blocks +// rename to OptIn validators? +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 + + if _, found := k.GetValidatorConsumerPubKey(ctx, chainID, types.NewProviderConsAddress(valConsAddr)); !found { + 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)) + reward := fees.MulDecTruncate(powerFraction) + + k.distributionKeeper.AllocateTokensToValidator( + ctx, + k.stakingKeeper.ValidatorByConsAddr(ctx, valConsAddr), + reward, + ) + totalReward = totalReward.Add(reward...) + } + + return +} + +func (k Keeper) GetConsumerFeeCollected(ctx sdk.Context, chainID string) (feesCollected sdk.Coins) { + // get whitelisted denoms for consumer chain + // note that they may differ per consumer in the future denoms := k.GetAllConsumerRewardDenoms(ctx) - // 2. Iterate over the whitelist + // sum the total fees collected for _, denom := range denoms { - // 3. For each denom, retrieve the balance from the consumer rewards pool balance := k.bankKeeper.GetBalance( ctx, - k.accountKeeper.GetModuleAccount(ctx, types.ConsumerRewardsPool).GetAddress(), + k.GetConsumerModuleAccountAddress(ctx, chainID), denom, ) - - // if the balance is not zero, - if !balance.IsZero() { - // 4. Transfer the balance to the fee collector address - err := k.bankKeeper.SendCoinsFromModuleToModule( - ctx, - types.ConsumerRewardsPool, - k.feeCollectorName, - sdk.NewCoins(balance), - ) - if err != nil { - k.Logger(ctx).Error("cannot sent consumer rewards to fee collector:", "reward", balance.String()) - } - } + feesCollected = feesCollected.Add(balance) } + + return +} + +func (k Keeper) GetConsumerModuleAccountAddress(ctx sdk.Context, chainID string) sdk.AccAddress { + return authtypes.NewModuleAddress(k.GetConsumerModuleAccount(ctx, chainID)) } diff --git a/x/ccv/provider/keeper/keeper.go b/x/ccv/provider/keeper/keeper.go index 510d957c5d..8752b558c4 100644 --- a/x/ccv/provider/keeper/keeper.go +++ b/x/ccv/provider/keeper/keeper.go @@ -339,28 +339,28 @@ func (k Keeper) DeleteConsumerGenesis(ctx sdk.Context, chainID string) { // VerifyConsumerChain verifies that the chain trying to connect on the channel handshake // is the expected consumer chain. -func (k Keeper) VerifyConsumerChain(ctx sdk.Context, channelID string, connectionHops []string) error { +func (k Keeper) VerifyConsumerChain(ctx sdk.Context, channelID string, connectionHops []string) (string, error) { if len(connectionHops) != 1 { - return errorsmod.Wrap(channeltypes.ErrTooManyConnectionHops, "must have direct connection to provider chain") + return "", errorsmod.Wrap(channeltypes.ErrTooManyConnectionHops, "must have direct connection to provider chain") } connectionID := connectionHops[0] clientID, tmClient, err := k.getUnderlyingClient(ctx, connectionID) if err != nil { - return err + return "", err } ccvClientId, found := k.GetConsumerClientId(ctx, tmClient.ChainId) if !found { - return errorsmod.Wrapf(ccv.ErrClientNotFound, "cannot find client for consumer chain %s", tmClient.ChainId) + return "", errorsmod.Wrapf(ccv.ErrClientNotFound, "cannot find client for consumer chain %s", tmClient.ChainId) } if ccvClientId != clientID { - return errorsmod.Wrapf(types.ErrInvalidConsumerClient, "CCV channel must be built on top of CCV client. expected %s, got %s", ccvClientId, clientID) + return "", errorsmod.Wrapf(types.ErrInvalidConsumerClient, "CCV channel must be built on top of CCV client. expected %s, got %s", ccvClientId, clientID) } // Verify that there isn't already a CCV channel for the consumer chain if prevChannel, ok := k.GetChainToChannel(ctx, tmClient.ChainId); ok { - return errorsmod.Wrapf(ccv.ErrDuplicateChannel, "CCV channel with ID: %s already created for consumer chain %s", prevChannel, tmClient.ChainId) + return "", errorsmod.Wrapf(ccv.ErrDuplicateChannel, "CCV channel with ID: %s already created for consumer chain %s", prevChannel, tmClient.ChainId) } - return nil + return tmClient.ChainId, nil } // SetConsumerChain ensures that the consumer chain has not already been @@ -1136,3 +1136,26 @@ func (k Keeper) GetAllRegisteredAndProposedChainIDs(ctx sdk.Context) []string { return allConsumerChains } + +func (k Keeper) GetConsumerModuleAccount(ctx sdk.Context, chainID string) string { + store := ctx.KVStore(k.storeKey) + return string(store.Get(types.ConsumerChainModuleAccountKey(chainID))) +} + +func (k Keeper) AssignNextRewardPoolToConsumer(ctx sdk.Context, chainID string) { + store := ctx.KVStore(k.storeKey) + pool := types.ConsumerRewardPools[k.getAndIncrementConsumerModuleAccountIndex(ctx)] + store.Set(types.ConsumerChainModuleAccountKey(chainID), []byte(pool)) +} + +func (k Keeper) getAndIncrementConsumerModuleAccountIndex(ctx sdk.Context) (toReturn uint64) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.ConsumerChainModuleAccountIndexKey()) + if bz != nil { + toReturn = sdk.BigEndianToUint64(bz) + } + toStore := toReturn + 1 + store.Set(types.ConsumerChainModuleAccountIndexKey(), sdk.Uint64ToBigEndian(toStore)) + + return toReturn +} diff --git a/x/ccv/provider/module.go b/x/ccv/provider/module.go index f34b92bb07..8906a83bbd 100644 --- a/x/ccv/provider/module.go +++ b/x/ccv/provider/module.go @@ -144,6 +144,8 @@ func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { am.keeper.BeginBlockCCR(ctx) // Check for replenishing slash meter before any slash packets are processed for this block am.keeper.BeginBlockCIS(ctx) + // logic need for the Reward Distribution sub-protocol + am.keeper.BeginBlockRD(ctx, req) } // EndBlock implements the AppModule interface @@ -155,8 +157,6 @@ func (am AppModule) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.V am.keeper.EndBlockCCR(ctx) // EndBlock logic needed for the Validator Set Update sub-protocol am.keeper.EndBlockVSU(ctx) - // EndBlock logic need for the Reward Distribution sub-protocol - am.keeper.EndBlockRD(ctx) return []abci.ValidatorUpdate{} } diff --git a/x/ccv/provider/types/keys.go b/x/ccv/provider/types/keys.go index 615b901368..053646654e 100644 --- a/x/ccv/provider/types/keys.go +++ b/x/ccv/provider/types/keys.go @@ -28,9 +28,6 @@ const ( // Default validator set update ID DefaultValsetUpdateID = 1 - - // This address receives rewards from consumer chains - ConsumerRewardsPool = "consumer_rewards_pool" ) // Iota generated keys/byte prefixes (as a byte), supports 256 possible values @@ -145,9 +142,23 @@ const ( // ProposedConsumerChainByteKey is the byte prefix storing the consumer chainId in consumerAddition gov proposal submitted before voting finishes ProposedConsumerChainByteKey + // ConsumerChainRewardPoolIndexByteKey + ConsumerChainModuleAccountIndexByteKey // NOTE: DO NOT ADD NEW BYTE PREFIXES HERE WITHOUT ADDING THEM TO getAllKeyPrefixes() IN keys_test.go + + // ConsumerChainRewardPoolBytePrefix + ConsumerChainModuleAccountBytePrefix ) +// ConsumerRewardPools contains the module account that receives rewards from consumer chains +var ConsumerRewardPools = []string{ + "consumer_rewards_pool_0", + "consumer_rewards_pool_1", + "consumer_rewards_pool_2", + "consumer_rewards_pool_3", + "consumer_rewards_pool_4", +} + // // Fully defined key func section // @@ -517,6 +528,16 @@ func ParseProposedConsumerChainKey(prefix byte, bz []byte) (uint64, error) { return proposalID, nil } +// ConsumerModuleAccount returns the module account byte prefix for a consumer chain +func ConsumerChainModuleAccountKey(chainID string) []byte { + k := append([]byte{ConsumerChainModuleAccountBytePrefix}, []byte(chainID)...) + return k +} + +func ConsumerChainModuleAccountIndexKey() []byte { + return []byte{ConsumerChainModuleAccountIndexByteKey} +} + // // End of generic helpers section // diff --git a/x/ccv/provider/types/keys_test.go b/x/ccv/provider/types/keys_test.go index 4d5ea58ff8..859af59a25 100644 --- a/x/ccv/provider/types/keys_test.go +++ b/x/ccv/provider/types/keys_test.go @@ -56,6 +56,8 @@ func getAllKeyPrefixes() []byte { providertypes.VSCMaturedHandledThisBlockBytePrefix, providertypes.EquivocationEvidenceMinHeightBytePrefix, providertypes.ProposedConsumerChainByteKey, + providertypes.ConsumerChainModuleAccountIndexByteKey, + providertypes.ConsumerChainModuleAccountBytePrefix, } } diff --git a/x/ccv/types/expected_keepers.go b/x/ccv/types/expected_keepers.go index a2ef7ab465..856e5fe947 100644 --- a/x/ccv/types/expected_keepers.go +++ b/x/ccv/types/expected_keepers.go @@ -4,6 +4,8 @@ import ( context "context" "time" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" conntypes "github.com/cosmos/ibc-go/v7/modules/core/03-connection/types" @@ -109,6 +111,10 @@ type ClientKeeper interface { // DistributionKeeper defines the expected interface of the distribution keeper type DistributionKeeper interface { FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error + GetFeePool(ctx sdk.Context) distrtypes.FeePool + SetFeePool(ctx sdk.Context, feePool distrtypes.FeePool) + GetCommunityTax(ctx sdk.Context) math.LegacyDec + AllocateTokensToValidator(ctx sdk.Context, validator stakingtypes.ValidatorI, reward sdk.DecCoins) } // ConsumerHooks event hooks for newly bonded cross-chain validators