From 18ba837ca002829856a221ec64d45dfcdf5b2b9e Mon Sep 17 00:00:00 2001 From: Marius Poke Date: Fri, 20 Sep 2024 18:07:11 +0200 Subject: [PATCH] fix!: enable the distribution of ICS rewards from Stride (#2288) * enable Stride to distribute ICS rewards * fix bug in migration * assign to same consumerId * add changelog entries --- .../bug-fixes/2288-rewards-stride.md | 2 + .../state-breaking/2288-rewards-stride.md | 2 + x/ccv/provider/ibc_middleware.go | 76 ++++++++++++++++- x/ccv/provider/keeper/distribution.go | 83 ++++++++++++++++--- x/ccv/provider/migrations/v8/migrations.go | 6 +- x/ccv/provider/types/events.go | 8 ++ x/ccv/types/events.go | 1 - 7 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 .changelog/unreleased/bug-fixes/2288-rewards-stride.md create mode 100644 .changelog/unreleased/state-breaking/2288-rewards-stride.md diff --git a/.changelog/unreleased/bug-fixes/2288-rewards-stride.md b/.changelog/unreleased/bug-fixes/2288-rewards-stride.md new file mode 100644 index 0000000000..997998e8eb --- /dev/null +++ b/.changelog/unreleased/bug-fixes/2288-rewards-stride.md @@ -0,0 +1,2 @@ +- `[x/provider]` Add patch to enable ICS rewards from Stride to be distributed. + ([\#2288](https://github.com/cosmos/interchain-security/pull/2288)) \ No newline at end of file diff --git a/.changelog/unreleased/state-breaking/2288-rewards-stride.md b/.changelog/unreleased/state-breaking/2288-rewards-stride.md new file mode 100644 index 0000000000..997998e8eb --- /dev/null +++ b/.changelog/unreleased/state-breaking/2288-rewards-stride.md @@ -0,0 +1,2 @@ +- `[x/provider]` Add patch to enable ICS rewards from Stride to be distributed. + ([\#2288](https://github.com/cosmos/interchain-security/pull/2288)) \ No newline at end of file diff --git a/x/ccv/provider/ibc_middleware.go b/x/ccv/provider/ibc_middleware.go index 5ba56949aa..d5db2fd50e 100644 --- a/x/ccv/provider/ibc_middleware.go +++ b/x/ccv/provider/ibc_middleware.go @@ -113,6 +113,8 @@ func (im IBCMiddleware) OnRecvPacket( packet channeltypes.Packet, relayer sdk.AccAddress, ) exported.Acknowledgement { + logger := im.keeper.Logger(ctx) + // executes the IBC transfer OnRecv logic ack := im.app.OnRecvPacket(ctx, packet, relayer) @@ -124,7 +126,29 @@ func (im IBCMiddleware) OnRecvPacket( // execute the middleware logic only if the sender is a consumer chain consumerId, err := im.keeper.IdentifyConsumerIdFromIBCPacket(ctx, packet) if err != nil { - return ack + // Check if the packet is received on a canonical transfer channels + // of one of the known consumer chains. + // Note: this is a patch for the Cosmos Hub for consumers such as Stride + // TODO: remove once the known consumer chains upgrade to send ICS rewards + // with the consumer ID added to the memo field + if ctx.ChainID() == "cosmoshub-4" && // this patch is only for the Cosmos Hub + packet.DestinationChannel == "channel-391" { // canonical transfer channel Stride <> Cosmos Hub + // check source chain ID + srcChainId, err := im.keeper.GetSourceChainIdFromIBCPacket(ctx, packet) + if err != nil || srcChainId != "stride-1" { + // ignore packet if it's not from Stride + return ack + } + // accept the packet as a potential ICS reward + consumerId = "1" // consumer ID of Stride + // sanity check: make sure this is the consumer ID for Stride + chainId, err := im.keeper.GetConsumerChainId(ctx, consumerId) + if err != nil || srcChainId != chainId { + return ack + } + } else { + return ack + } } // extract the coin info received from the packet data @@ -137,9 +161,41 @@ func (im IBCMiddleware) OnRecvPacket( return ack } + chainId, err := im.keeper.GetConsumerChainId(ctx, consumerId) + if err != nil { + logger.Error( + "cannot get consumer chain id in transfer middleware", + "consumerId", consumerId, + "packet", packet.String(), + "fungibleTokenPacketData", data.String(), + "error", err.Error(), + ) + return ack + } + coinAmt, _ := math.NewIntFromString(data.Amount) coinDenom := GetProviderDenom(data.Denom, packet) + logger.Info( + "received ICS rewards from consumer chain", + "consumerId", consumerId, + "chainId", chainId, + "denom", coinDenom, + "amount", data.Amount, + ) + + // initialize an empty slice to store event attributes + eventAttributes := []sdk.Attribute{} + + // add event attributes + eventAttributes = append(eventAttributes, []sdk.Attribute{ + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeConsumerId, consumerId), + sdk.NewAttribute(types.AttributeConsumerChainId, chainId), + sdk.NewAttribute(types.AttributeRewardDenom, coinDenom), + sdk.NewAttribute(types.AttributeRewardAmount, data.Amount), + }...) + // verify that the coin's denom is a whitelisted consumer denom, // and if so, adds it to the consumer chain rewards allocation, // otherwise the prohibited coin just stays in the pool forever. @@ -151,7 +207,25 @@ func (im IBCMiddleware) OnRecvPacket( Amount: coinAmt, })...) im.keeper.SetConsumerRewardsAllocation(ctx, consumerId, alloc) + + logger.Info( + "scheduled ICS rewards to be distributed", + "consumerId", consumerId, + "chainId", chainId, + "denom", coinDenom, + "amount", data.Amount, + ) + + // add RewardDistribution event attribute + eventAttributes = append(eventAttributes, sdk.NewAttribute(types.AttributeRewardDistribution, "scheduled")) } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeUpdateConsumer, + eventAttributes..., + ), + ) } return ack diff --git a/x/ccv/provider/keeper/distribution.go b/x/ccv/provider/keeper/distribution.go index 85241c794a..3d47813112 100644 --- a/x/ccv/provider/keeper/distribution.go +++ b/x/ccv/provider/keeper/distribution.go @@ -79,7 +79,6 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { // To avoid large iterations over all the consumer IDs, iterate only over // chains with an IBC client created. for _, consumerId := range k.GetAllConsumersWithIBCClients(ctx) { - // note that it's possible that no rewards are collected even though the // reward pool isn't empty. This can happen if the reward pool holds some tokens // of non-whitelisted denominations. @@ -88,6 +87,16 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { continue } + chainId, err := k.GetConsumerChainId(ctx, consumerId) + if err != nil { + k.Logger(ctx).Error( + "cannot get consumer chain id in AllocateTokens", + "consumerId", consumerId, + "error", err.Error(), + ) + continue + } + // temporary workaround to keep CanWithdrawInvariant happy // general discussions here: https://github.com/cosmos/cosmos-sdk/issues/2906#issuecomment-441867634 if k.ComputeConsumerTotalVotingPower(ctx, consumerId) == 0 { @@ -95,11 +104,18 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { err := k.distributionKeeper.FundCommunityPool(context.Context(ctx), rewardsToSend, k.accountKeeper.GetModuleAccount(ctx, types.ConsumerRewardsPool).GetAddress()) if err != nil { k.Logger(ctx).Error( - "fail to allocate rewards from consumer chain %s to community pool: %s", - consumerId, - err, + "fail to allocate ICS rewards to community pool", + "consumerId", consumerId, + "chainId", chainId, + "error", err.Error(), ) } + k.Logger(ctx).Info( + "allocated ICS rewards to community pool", + "consumerId", consumerId, + "chainId", chainId, + "amount", rewardsToSend.String(), + ) // set the consumer allocation to the remaining reward decimals alloc.Rewards = rewardsChange @@ -114,9 +130,10 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { communityTax, err := k.distributionKeeper.GetCommunityTax(ctx) if err != nil { k.Logger(ctx).Error( - "cannot get community tax while allocating rewards from consumer chain %s: %s", - consumerId, - err, + "cannot get community tax while allocating ICS rewards", + "consumerId", consumerId, + "chainId", chainId, + "error", err.Error(), ) continue } @@ -134,9 +151,10 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ConsumerRewardsPool, distrtypes.ModuleName, validatorsRewardsTrunc) if err != nil { k.Logger(ctx).Error( - "cannot send rewards to distribution module account %s: %s", - consumerId, - err, + "cannot send ICS rewards to distribution module account", + "consumerId", consumerId, + "chainId", chainId, + "error", err.Error(), ) continue } @@ -153,9 +171,10 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { err = k.distributionKeeper.FundCommunityPool(context.Context(ctx), remainingRewards, k.accountKeeper.GetModuleAccount(ctx, types.ConsumerRewardsPool).GetAddress()) if err != nil { k.Logger(ctx).Error( - "fail to allocate rewards from consumer chain %s to community pool: %s", - consumerId, - err, + "fail to allocate ICS rewards to community pool", + "consumerId", consumerId, + "chainId", chainId, + "error", err.Error(), ) continue } @@ -163,6 +182,27 @@ func (k Keeper) AllocateTokens(ctx sdk.Context) { // set consumer allocations to the remaining rewards decimals alloc.Rewards = validatorsRewardsChange.Add(remainingChanges...) k.SetConsumerRewardsAllocation(ctx, consumerId, alloc) + + k.Logger(ctx).Info( + "distributed ICS rewards successfully", + "consumerId", consumerId, + "chainId", chainId, + "total-rewards", consumerRewards.String(), + "sent-to-distribution", validatorsRewardsTrunc.String(), + "sent-to-CP", remainingRewards.String(), + ) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeDistributedRewards, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute(types.AttributeConsumerId, consumerId), + sdk.NewAttribute(types.AttributeConsumerChainId, chainId), + sdk.NewAttribute(types.AttributeRewardTotal, consumerRewards.String()), + sdk.NewAttribute(types.AttributeRewardDistributed, validatorsRewardsTrunc.String()), + sdk.NewAttribute(types.AttributeRewardCommunityPool, remainingRewards.String()), + ), + ) } } @@ -341,6 +381,23 @@ func (k Keeper) IdentifyConsumerIdFromIBCPacket(ctx sdk.Context, packet channelt return consumerId, nil } +// GetSourceChainIdFromIBCPacket returns the chain ID of the chain that sent this packet +func (k Keeper) GetSourceChainIdFromIBCPacket(ctx sdk.Context, packet channeltypes.Packet) (string, error) { + channel, ok := k.channelKeeper.GetChannel(ctx, packet.DestinationPort, packet.DestinationChannel) + if !ok { + return "", errorsmod.Wrapf(channeltypes.ErrChannelNotFound, "channel not found for channel ID: %s", packet.DestinationChannel) + } + if len(channel.ConnectionHops) != 1 { + return "", errorsmod.Wrap(channeltypes.ErrTooManyConnectionHops, "must have direct connection to consumer chain") + } + connectionID := channel.ConnectionHops[0] + _, tmClient, err := k.getUnderlyingClient(ctx, connectionID) + if err != nil { + return "", err + } + return tmClient.ChainId, nil +} + // HandleSetConsumerCommissionRate sets a per-consumer chain commission rate for the given provider address // on the condition that the given consumer chain exists. func (k Keeper) HandleSetConsumerCommissionRate(ctx sdk.Context, consumerId string, providerAddr types.ProviderConsAddress, commissionRate math.LegacyDec) error { diff --git a/x/ccv/provider/migrations/v8/migrations.go b/x/ccv/provider/migrations/v8/migrations.go index 8bf3c4ec18..c7b94c95e8 100644 --- a/x/ccv/provider/migrations/v8/migrations.go +++ b/x/ccv/provider/migrations/v8/migrations.go @@ -156,10 +156,10 @@ func MigrateLaunchedConsumerChains(ctx sdk.Context, store storetypes.KVStore, pk // channelId -> chainId channelId, found := pk.GetConsumerIdToChannelId(ctx, chainId) - if !found { - return errorsmod.Wrapf(ccv.ErrInvalidConsumerState, "cannot find channel id associated with consumer id: %s", consumerId) + if found { + // if not found, then the CCV channel was not yet established + pk.SetChannelToConsumerId(ctx, channelId, consumerId) } - pk.SetChannelToConsumerId(ctx, channelId, consumerId) // chainId -> channelId rekeyFromChainIdToConsumerId(store, LegacyChainToChannelKeyPrefix, chainId, consumerId) diff --git a/x/ccv/provider/types/events.go b/x/ccv/provider/types/events.go index 5583174e42..ff5cb5a05d 100644 --- a/x/ccv/provider/types/events.go +++ b/x/ccv/provider/types/events.go @@ -12,6 +12,8 @@ const ( EventTypeCreateConsumer = "create_consumer" EventTypeUpdateConsumer = "update_consumer" EventTypeRemoveConsumer = "remove_consumer" + EventTypeReceivedRewards = "received_ics_rewards" + EventTypeDistributedRewards = "distributed_ics_rewards" AttributeInfractionHeight = "infraction_height" AttributeInitialHeight = "initial_height" @@ -31,4 +33,10 @@ const ( AttributeConsumerSpawnTime = "consumer_spawn_time" AttributeConsumerPhase = "consumer_phase" AttributeConsumerTopN = "consumer_topn" + AttributeRewardDenom = "reward_denom" + AttributeRewardAmount = "reward_amount" + AttributeRewardDistribution = "reward_distribution" + AttributeRewardTotal = "total_rewards" + AttributeRewardDistributed = "distributed_rewards" + AttributeRewardCommunityPool = "community_pool_rewards" ) diff --git a/x/ccv/types/events.go b/x/ccv/types/events.go index 8ee8388eb3..bc949b67e8 100644 --- a/x/ccv/types/events.go +++ b/x/ccv/types/events.go @@ -13,7 +13,6 @@ const ( EventTypeSubmitConsumerMisbehaviour = "submit_consumer_misbehaviour" EventTypeSubmitConsumerDoubleVoting = "submit_consumer_double_voting" EventTypeExecuteConsumerChainSlash = "execute_consumer_chain_slash" - EventTypeFeeDistribution = "fee_distribution" EventTypeConsumerSlashRequest = "consumer_slash_request" AttributeKeyAckSuccess = "success"