Skip to content

Commit 33ea185

Browse files
authored
feat!: enable chains to allowlist reward denoms permissionlessly (#2309)
* move big chunk of AllocateTokens to new AllocateConsumerRewards method * update protos to included allowlisted denoms * added state and main functionality * fixed integration tests * add changelogs * read the returned error * took into account comments * remove unused state
1 parent d0a1398 commit 33ea185

File tree

16 files changed

+1034
-535
lines changed

16 files changed

+1034
-535
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- `[x/provider]` Enable permissionless allowlisting of reward denoms (at most 3) per consumer chain.
2+
([\#2309](https://github.com/cosmos/interchain-security/pull/2309))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- `[x/provider]` Enable permissionless allowlisting of reward denoms (at most 3) per consumer chain.
2+
([\#2309](https://github.com/cosmos/interchain-security/pull/2309))

proto/interchain_security/ccv/provider/v1/provider.proto

+3
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,6 @@ enum ConsumerPhase {
515515
// DELETED defines the phase in which the state of a stopped chain has been deleted.
516516
CONSUMER_PHASE_DELETED = 5;
517517
}
518+
519+
// AllowlistedRewardDenoms corresponds to the denoms allowlisted by a specific consumer id
520+
message AllowlistedRewardDenoms { repeated string denoms = 1; }

proto/interchain_security/ccv/provider/v1/tx.proto

+6
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ message MsgCreateConsumer {
360360
ConsumerInitializationParameters initialization_parameters = 4;
361361

362362
PowerShapingParameters power_shaping_parameters = 5;
363+
364+
// allowlisted reward denoms of the consumer
365+
AllowlistedRewardDenoms allowlisted_reward_denoms = 7;
363366
}
364367

365368
// MsgCreateConsumerResponse defines response type for MsgCreateConsumer
@@ -388,6 +391,9 @@ message MsgUpdateConsumer {
388391

389392
// the power-shaping parameters of the consumer when updated
390393
PowerShapingParameters power_shaping_parameters = 6;
394+
395+
// allowlisted reward denoms of the consumer (if provided they overwrite previously set reward denoms)
396+
AllowlistedRewardDenoms allowlisted_reward_denoms = 7;
391397
}
392398

393399
// MsgUpdateConsumerResponse defines response type for MsgUpdateConsumer messages

tests/integration/distribution.go

+52-53
Original file line numberDiff line numberDiff line change
@@ -107,21 +107,37 @@ func (s *CCVTestSuite) TestRewardsDistribution() {
107107
rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool)
108108
s.Require().Equal(rewardCoins.AmountOf(rewardsIBCdenom), providerExpRewardsAmount)
109109

110-
// Set the consumer reward denom. This would be done by a governance proposal in prod.
110+
// increase the block height so validators are eligible for consumer rewards (see `IsEligibleForConsumerRewards`)
111+
currentProviderBlockHeight := s.providerCtx().BlockHeight()
112+
numberOfBlocksToStartReceivingRewards := providerKeeper.GetNumberOfEpochsToStartReceivingRewards(s.providerCtx()) * providerKeeper.GetBlocksPerEpoch(s.providerCtx())
113+
for s.providerCtx().BlockHeight() <= currentProviderBlockHeight+numberOfBlocksToStartReceivingRewards {
114+
s.providerChain.NextBlock()
115+
}
116+
117+
// Allowlist the consumer reward denom. This would be done by a governance proposal in prod.
111118
providerKeeper.SetConsumerRewardDenom(s.providerCtx(), rewardsIBCdenom)
112119

113-
// Refill the consumer fee pool
114-
err = consumerBankKeeper.SendCoinsFromAccountToModule(
115-
s.consumerCtx(),
116-
s.consumerChain.SenderAccount.GetAddress(),
117-
authtypes.FeeCollectorName,
118-
fees,
119-
)
120+
// Even though the reward denom is allowlisted, the rewards are not yet distributed because `BeginBlockRD` has not executed.
121+
// and hence the distribution has not taken place. Because of this, all the rewards are still stored in consumer rewards allocation by denom.
122+
rewardsAlloc, err := providerKeeper.GetConsumerRewardsAllocationByDenom(s.providerCtx(), s.getFirstBundle().ConsumerId, rewardsIBCdenom)
120123
s.Require().NoError(err)
124+
remainingAlloc := rewardsAlloc.Rewards.AmountOf(rewardsIBCdenom)
125+
s.Require().Equal(
126+
math.LegacyNewDecFromInt(providerExpRewardsAmount),
127+
rewardsAlloc.Rewards.AmountOf(rewardsIBCdenom),
128+
)
129+
s.Require().False(remainingAlloc.LTE(math.LegacyOneDec()))
121130

122-
// Pass two blocks
123-
s.consumerChain.NextBlock()
124-
s.consumerChain.NextBlock()
131+
// Move by one block to execute `BeginBlockRD`.
132+
s.providerChain.NextBlock()
133+
134+
// Consumer allocations are now distributed between the validators and the community pool.
135+
// The decimals resulting from the distribution are expected to remain in the consumer allocations
136+
// and hence the check that remainingAlloc is less than one.
137+
rewardsAlloc, err = providerKeeper.GetConsumerRewardsAllocationByDenom(s.providerCtx(), s.getFirstBundle().ConsumerId, rewardsIBCdenom)
138+
s.Require().NoError(err)
139+
remainingAlloc = rewardsAlloc.Rewards.AmountOf(rewardsIBCdenom)
140+
s.Require().True(remainingAlloc.LTE(math.LegacyOneDec()))
125141

126142
// Save the consumer validators total outstanding rewards on the provider
127143
consumerValsOutstandingRewardsFunc := func(ctx sdk.Context) sdk.DecCoins {
@@ -137,45 +153,17 @@ func (s *CCVTestSuite) TestRewardsDistribution() {
137153
valReward, _ := providerDistributionKeeper.GetValidatorOutstandingRewards(ctx, valAddr)
138154
totalRewards = totalRewards.Add(valReward.Rewards...)
139155
}
140-
return totalRewards
141-
}
142-
consuValsRewards := consumerValsOutstandingRewardsFunc(s.providerCtx())
143156

144-
// increase the block height so validators are eligible for consumer rewards (see `IsEligibleForConsumerRewards`)
145-
numberOfBlocksToStartReceivingRewards := providerKeeper.GetNumberOfEpochsToStartReceivingRewards(s.providerCtx()) * providerKeeper.GetBlocksPerEpoch(s.providerCtx())
146-
147-
for s.providerCtx().BlockHeight() <= numberOfBlocksToStartReceivingRewards {
148-
s.providerChain.NextBlock()
157+
return totalRewards
149158
}
150159

151-
// Transfer rewards from consumer to provider and distribute rewards to
152-
// validators and community pool by calling BeginBlockRD
153-
relayAllCommittedPackets(
154-
s,
155-
s.consumerChain,
156-
s.transferPath,
157-
transfertypes.PortID,
158-
s.transferPath.EndpointA.ChannelID,
159-
1,
160-
)
161-
162-
// Consumer allocations are distributed between the validators and the community pool.
163-
// The decimals resulting from the distribution are expected to remain in the consumer allocations.
164-
rewardsAlloc := providerKeeper.GetConsumerRewardsAllocation(s.providerCtx(), s.getFirstBundle().ConsumerId)
165-
remainingAlloc := rewardsAlloc.Rewards.AmountOf(rewardsIBCdenom)
166-
s.Require().True(remainingAlloc.LTE(math.LegacyOneDec()))
167-
168-
// Check that the reward pool still holds the coins from the first transfer
169-
// which were never allocated since they were not whitelisted
170-
// plus the remaining decimals from the second transfer.
160+
// Check that the reward pool does not hold the coins from the transfer because
161+
// after the coin got allowlisted, all coins of this denom were distributed.
171162
rewardCoins = providerBankKeeper.GetAllBalances(s.providerCtx(), rewardPool)
172-
s.Require().Equal(
173-
math.LegacyNewDecFromInt(rewardCoins.AmountOf(rewardsIBCdenom)),
174-
math.LegacyNewDecFromInt(providerExpRewardsAmount).Add(remainingAlloc),
175-
)
163+
s.Require().True(rewardCoins.AmountOf(rewardsIBCdenom).LTE(math.NewInt(1)))
176164

177165
// Check that the distribution module account balance is equal to the consumer rewards
178-
consuValsRewardsReceived := consumerValsOutstandingRewardsFunc(s.providerCtx()).Sub(consuValsRewards)
166+
consuValsRewardsReceived := consumerValsOutstandingRewardsFunc(s.providerCtx())
179167
distrAcct := providerDistributionKeeper.GetDistributionAccount(s.providerCtx())
180168
distrAcctBalance := providerBankKeeper.GetAllBalances(s.providerCtx(), distrAcct.GetAddress())
181169

@@ -259,7 +247,7 @@ func (s *CCVTestSuite) TestSendRewardsRetries() {
259247
}
260248

261249
// TestEndBlockRD tests that the last transmission block height (LTBH) is correctly updated after the expected
262-
// number of block have passed. It also checks that the IBC transfer transfer states are discarded if
250+
// number of block have passed. It also checks that the IBC transfer states are discarded if
263251
// the reward distribution to the provider has failed.
264252
//
265253
// Note: this method is effectively a unit test for EndBLockRD(), but is written as an integration test to avoid excessive mocking.
@@ -570,7 +558,7 @@ func (s *CCVTestSuite) TestIBCTransferMiddleware() {
570558
{
571559
"IBC Transfer coin denom isn't registered",
572560
func(ctx sdk.Context, keeper *providerkeeper.Keeper, bankKeeper icstestingutils.TestBankKeeper) {},
573-
false,
561+
true, // even if the denom is not registered/allowlisted, the rewards are still allocated by denom
574562
false,
575563
},
576564
{
@@ -600,9 +588,10 @@ func (s *CCVTestSuite) TestIBCTransferMiddleware() {
600588
sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100_000))),
601589
)
602590
// update consumer allocation
603-
keeper.SetConsumerRewardsAllocation(
591+
keeper.SetConsumerRewardsAllocationByDenom(
604592
ctx,
605593
s.getFirstBundle().ConsumerId,
594+
getIBCDenom(packet.DestinationPort, packet.DestinationChannel),
606595
providertypes.ConsumerRewardsAllocation{
607596
Rewards: sdk.NewDecCoins(sdk.NewDecCoin(sdk.DefaultBondDenom, math.NewInt(100_000))),
608597
},
@@ -669,22 +658,25 @@ func (s *CCVTestSuite) TestIBCTransferMiddleware() {
669658
rewardsPoolBalance := bankKeeper.GetAllBalances(s.providerCtx(), sdk.MustAccAddressFromBech32(data.Receiver))
670659

671660
// save the consumer's rewards allocated
672-
consumerRewardsAllocations := providerKeeper.GetConsumerRewardsAllocation(s.providerCtx(), s.getFirstBundle().ConsumerId)
661+
ibcDenom := getIBCDenom(packet.DestinationPort, packet.DestinationChannel)
662+
consumerRewardsAllocations, err := providerKeeper.GetConsumerRewardsAllocationByDenom(s.providerCtx(), s.getFirstBundle().ConsumerId, ibcDenom)
663+
s.Require().NoError(err)
673664

674665
// execute middleware OnRecvPacket logic
675666
ack := cbs.OnRecvPacket(s.providerCtx(), packet, sdk.AccAddress{})
676667

677668
// compute expected rewards with provider denom
678669
expRewards := sdk.Coin{
679670
Amount: amount,
680-
Denom: getIBCDenom(packet.DestinationPort, packet.DestinationChannel),
671+
Denom: ibcDenom,
681672
}
682673

683674
// compute the balance and allocation difference
684675
rewardsTransferred := bankKeeper.GetAllBalances(s.providerCtx(), sdk.MustAccAddressFromBech32(data.Receiver)).
685676
Sub(rewardsPoolBalance...)
686-
rewardsAllocated := providerKeeper.GetConsumerRewardsAllocation(s.providerCtx(), s.getFirstBundle().ConsumerId).
687-
Rewards.Sub(consumerRewardsAllocations.Rewards)
677+
rewardsAllocatedByDenom, err := providerKeeper.GetConsumerRewardsAllocationByDenom(s.providerCtx(), s.getFirstBundle().ConsumerId, ibcDenom)
678+
s.Require().NoError(err)
679+
rewardsAllocated := rewardsAllocatedByDenom.Rewards.Sub(consumerRewardsAllocations.Rewards)
688680

689681
if !tc.expErr {
690682
s.Require().True(ack.Success())
@@ -738,13 +730,18 @@ func (s *CCVTestSuite) TestAllocateTokens() {
738730
totalRewards,
739731
)
740732

733+
// Allowlist a reward denom that the allocated rewards have
734+
ibcDenom := "ibc/somedenom"
735+
providerKeeper.SetConsumerRewardDenom(providerCtx, ibcDenom)
736+
741737
// Allocate rewards evenly between consumers
742738
rewardsPerChain := totalRewards.QuoInt(math.NewInt(int64(len(s.consumerBundles))))
743739
for consumerId := range s.consumerBundles {
744740
// update consumer allocation
745-
providerKeeper.SetConsumerRewardsAllocation(
741+
providerKeeper.SetConsumerRewardsAllocationByDenom(
746742
providerCtx,
747743
consumerId,
744+
ibcDenom,
748745
providertypes.ConsumerRewardsAllocation{
749746
Rewards: sdk.NewDecCoinsFromCoins(rewardsPerChain...),
750747
},
@@ -798,7 +795,9 @@ func (s *CCVTestSuite) TestAllocateTokens() {
798795
// check that the total expected rewards are transferred to the distribution module account
799796

800797
// store the decimal remainders in the consumer reward allocations
801-
allocRemainderPerChain := providerKeeper.GetConsumerRewardsAllocation(providerCtx, s.getFirstBundle().ConsumerId).Rewards
798+
allocRemainderPerChainRes, err := providerKeeper.GetConsumerRewardsAllocationByDenom(providerCtx, s.getFirstBundle().ConsumerId, ibcDenom)
799+
s.Require().NoError(err)
800+
allocRemainderPerChain := allocRemainderPerChainRes.Rewards
802801

803802
// compute the total rewards distributed to the distribution module balance (validator outstanding rewards + community pool tax),
804803
totalRewardsDistributed := sdk.NewDecCoinsFromCoins(totalRewards...).Sub(allocRemainderPerChain.MulDec(math.LegacyNewDec(int64(consNum))))

x/ccv/provider/ibc_middleware.go

+36-18
Original file line numberDiff line numberDiff line change
@@ -192,30 +192,48 @@ func (im IBCMiddleware) OnRecvPacket(
192192
sdk.NewAttribute(types.AttributeRewardAmount, data.Amount),
193193
}...)
194194

195-
// verify that the coin's denom is a whitelisted consumer denom,
196-
// and if so, adds it to the consumer chain rewards allocation,
197-
// otherwise the prohibited coin just stays in the pool forever.
198-
if im.keeper.ConsumerRewardDenomExists(ctx, coinDenom) {
199-
alloc := im.keeper.GetConsumerRewardsAllocation(ctx, consumerId)
200-
alloc.Rewards = alloc.Rewards.Add(
201-
sdk.NewDecCoinsFromCoins(sdk.Coin{
202-
Denom: coinDenom,
203-
Amount: coinAmt,
204-
})...)
205-
im.keeper.SetConsumerRewardsAllocation(ctx, consumerId, alloc)
206-
207-
logger.Info(
208-
"scheduled ICS rewards to be distributed",
195+
alloc, err := im.keeper.GetConsumerRewardsAllocationByDenom(ctx, consumerId, coinDenom)
196+
if err != nil {
197+
logger.Error(
198+
"cannot get consumer rewards by denom",
209199
"consumerId", consumerId,
210-
"chainId", chainId,
200+
"packet", packet.String(),
201+
"fungibleTokenPacketData", data.String(),
211202
"denom", coinDenom,
212-
"amount", data.Amount,
203+
"error", err.Error(),
213204
)
205+
return ack
206+
}
214207

215-
// add RewardDistribution event attribute
216-
eventAttributes = append(eventAttributes, sdk.NewAttribute(types.AttributeRewardDistribution, "scheduled"))
208+
alloc.Rewards = alloc.Rewards.Add(
209+
sdk.NewDecCoinFromCoin(sdk.Coin{
210+
Denom: coinDenom,
211+
Amount: coinAmt,
212+
}))
213+
err = im.keeper.SetConsumerRewardsAllocationByDenom(ctx, consumerId, coinDenom, alloc)
214+
if err != nil {
215+
logger.Error(
216+
"cannot set consumer rewards by denom",
217+
"consumerId", consumerId,
218+
"packet", packet.String(),
219+
"fungibleTokenPacketData", data.String(),
220+
"denom", coinDenom,
221+
"error", err.Error(),
222+
)
223+
return ack
217224
}
218225

226+
logger.Info(
227+
"scheduled ICS rewards to be distributed",
228+
"consumerId", consumerId,
229+
"chainId", chainId,
230+
"denom", coinDenom,
231+
"amount", data.Amount,
232+
)
233+
234+
// add RewardDistribution event attribute
235+
eventAttributes = append(eventAttributes, sdk.NewAttribute(types.AttributeRewardDistribution, "scheduled"))
236+
219237
ctx.EventManager().EmitEvent(
220238
sdk.NewEvent(
221239
types.EventTypeUpdateConsumer,

x/ccv/provider/keeper/consumer_lifecycle.go

-1
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId string) (err err
487487
k.DeleteAllOptedIn(ctx, consumerId)
488488
k.DeleteConsumerValSet(ctx, consumerId)
489489

490-
k.DeleteConsumerRewardsAllocation(ctx, consumerId)
491490
k.DeleteConsumerRemovalTime(ctx, consumerId)
492491

493492
// TODO (PERMISSIONLESS) add newly-added state to be deleted

0 commit comments

Comments
 (0)