Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: enable the distribution of ICS rewards from Stride (backport #2288) #2302

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changelog/unreleased/bug-fixes/2288-rewards-stride.md
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 2 additions & 0 deletions .changelog/unreleased/state-breaking/2288-rewards-stride.md
Original file line number Diff line number Diff line change
@@ -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))
76 changes: 75 additions & 1 deletion x/ccv/provider/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down
83 changes: 70 additions & 13 deletions x/ccv/provider/keeper/distribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -88,18 +87,35 @@ 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 {
rewardsToSend, rewardsChange := alloc.Rewards.TruncateDecimal()
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
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -153,16 +171,38 @@ 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
}

// 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()),
),
)
}
}

Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions x/ccv/provider/migrations/v8/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions x/ccv/provider/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)
1 change: 0 additions & 1 deletion x/ccv/types/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading