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

feat!: enable chains to allowlist reward denoms permissionlessly #2309

Merged
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,6 @@ enum ConsumerPhase {
// DELETED defines the phase in which the state of a stopped chain has been deleted.
CONSUMER_PHASE_DELETED = 5;
}

// AllowlistedRewardDenoms corresponds to the denoms allowlisted by a specific consumer id
message AllowlistedRewardDenoms { repeated string denoms = 1; }
7 changes: 6 additions & 1 deletion proto/interchain_security/ccv/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ message MsgChangeRewardDenoms {
repeated string denoms_to_remove = 2;
// authority is the address of the governance account
string authority = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"];

}

// MsgChangeRewardDenomsResponse defines response type for MsgChangeRewardDenoms messages
Expand Down Expand Up @@ -360,6 +359,9 @@ message MsgCreateConsumer {
ConsumerInitializationParameters initialization_parameters = 4;

PowerShapingParameters power_shaping_parameters = 5;

// allowlisted reward denoms of the consumer
AllowlistedRewardDenoms allowlisted_reward_denoms = 7;
}

// MsgCreateConsumerResponse defines response type for MsgCreateConsumer
Expand Down Expand Up @@ -388,6 +390,9 @@ message MsgUpdateConsumer {

// the power-shaping parameters of the consumer when updated
PowerShapingParameters power_shaping_parameters = 6;

// allowlisted reward denoms of the consumer (if provided they overwrite previously set reward denoms)
AllowlistedRewardDenoms allowlisted_reward_denoms = 7;
}

// MsgUpdateConsumerResponse defines response type for MsgUpdateConsumer messages
Expand Down
31 changes: 18 additions & 13 deletions x/ccv/provider/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,14 @@ func (im IBCMiddleware) OnRecvPacket(
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.
alloc2 := im.keeper.GetConsumerRewardsAllocationByDenom(ctx, consumerId, coinDenom)
alloc2.Rewards = alloc2.Rewards.Add(
sdk.NewDecCoinFromCoin(sdk.Coin{
Denom: coinDenom,
Amount: coinAmt,
}))
im.keeper.SetConsumerRewardsAllocationByDenom(ctx, consumerId, coinDenom, alloc2)

if im.keeper.ConsumerRewardDenomExists(ctx, coinDenom) {
alloc := im.keeper.GetConsumerRewardsAllocation(ctx, consumerId)
alloc.Rewards = alloc.Rewards.Add(
Expand All @@ -207,18 +212,18 @@ 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,
)
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"))
}
// add RewardDistribution event attribute
eventAttributes = append(eventAttributes, sdk.NewAttribute(types.AttributeRewardDistribution, "scheduled"))

ctx.EventManager().EmitEvent(
sdk.NewEvent(
Expand Down
258 changes: 149 additions & 109 deletions x/ccv/provider/keeper/distribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package keeper

import (
"context"

channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -67,142 +66,183 @@ func (k Keeper) GetAllConsumerRewardDenoms(ctx sdk.Context) (consumerRewardDenom
return consumerRewardDenoms
}

// AllocateTokens performs rewards distribution to the community pool and validators
// based on the Partial Set Security distribution specification.
func (k Keeper) AllocateTokens(ctx sdk.Context) {
// return if there is no coins in the consumer rewards pool
if k.GetConsumerRewardsPool(ctx).IsZero() {
return
// AllocateConsumerRewards allocates the given rewards to provider consumer chain with the given consumer id
func (k Keeper) AllocateConsumerRewards(ctx sdk.Context, consumerId string, alloc types.ConsumerRewardsAllocation) (types.ConsumerRewardsAllocation, error) {
if alloc.Rewards.IsZero() {
return types.ConsumerRewardsAllocation{}, nil
}

// Iterate over all launched consumer chains.
// 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.
alloc := k.GetConsumerRewardsAllocation(ctx, consumerId)
if alloc.Rewards.IsZero() {
continue
}
chainId, err := k.GetConsumerChainId(ctx, consumerId)
if err != nil {
k.Logger(ctx).Error(
"cannot get consumer chain id in AllocateConsumerRewards",
"consumerId", consumerId,
"error", err.Error(),
)
return types.ConsumerRewardsAllocation{}, err
}

chainId, err := k.GetConsumerChainId(ctx, consumerId)
// 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(
"cannot get consumer chain id in AllocateTokens",
"fail to allocate ICS rewards to community pool",
"consumerId", consumerId,
"chainId", chainId,
"error", err.Error(),
)
continue
}
k.Logger(ctx).Info(
"allocated ICS rewards to community pool",
"consumerId", consumerId,
"chainId", chainId,
"amount", rewardsToSend.String(),
)

// 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 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

// set the consumer allocation to the remaining reward decimals
alloc.Rewards = rewardsChange
k.SetConsumerRewardsAllocation(ctx, consumerId, alloc)
// WE DO NOT RETURN AN ERROR HERE because we want to update ...
return alloc, nil
}

return
}
// Consumer rewards are distributed between the validators and the community pool.
// The decimals resulting from the distribution are expected to remain in the consumer reward allocations.

// Consumer rewards are distributed between the validators and the community pool.
// The decimals resulting from the distribution are expected to remain in the consumer reward allocations.
communityTax, err := k.distributionKeeper.GetCommunityTax(ctx)
if err != nil {
k.Logger(ctx).Error(
"cannot get community tax while allocating ICS rewards",
"consumerId", consumerId,
"chainId", chainId,
"error", err.Error(),
)
return types.ConsumerRewardsAllocation{}, err
}

communityTax, err := k.distributionKeeper.GetCommunityTax(ctx)
if err != nil {
k.Logger(ctx).Error(
"cannot get community tax while allocating ICS rewards",
"consumerId", consumerId,
"chainId", chainId,
"error", err.Error(),
)
continue
}
// compute rewards for validators
consumerRewards := alloc.Rewards
voteMultiplier := math.LegacyOneDec().Sub(communityTax)
validatorsRewards := consumerRewards.MulDecTruncate(voteMultiplier)

// compute rewards for validators
consumerRewards := alloc.Rewards
voteMultiplier := math.LegacyOneDec().Sub(communityTax)
validatorsRewards := consumerRewards.MulDecTruncate(voteMultiplier)
// compute remaining rewards for the community pool
remaining := consumerRewards.Sub(validatorsRewards)

// compute remaining rewards for the community pool
remaining := consumerRewards.Sub(validatorsRewards)
// transfer validators rewards to distribution module account
validatorsRewardsTrunc, validatorsRewardsChange := validatorsRewards.TruncateDecimal()
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ConsumerRewardsPool, distrtypes.ModuleName, validatorsRewardsTrunc)
if err != nil {
k.Logger(ctx).Error(
"cannot send ICS rewards to distribution module account",
"consumerId", consumerId,
"chainId", chainId,
"error", err.Error(),
)
return types.ConsumerRewardsAllocation{}, err
}

// transfer validators rewards to distribution module account
validatorsRewardsTrunc, validatorsRewardsChange := validatorsRewards.TruncateDecimal()
err = k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ConsumerRewardsPool, distrtypes.ModuleName, validatorsRewardsTrunc)
if err != nil {
k.Logger(ctx).Error(
"cannot send ICS rewards to distribution module account",
"consumerId", consumerId,
"chainId", chainId,
"error", err.Error(),
)
continue
}
// allocate tokens to consumer validators
k.AllocateTokensToConsumerValidators(
ctx,
consumerId,
sdk.NewDecCoinsFromCoins(validatorsRewardsTrunc...),
)

// allocate tokens to consumer validators
k.AllocateTokensToConsumerValidators(
ctx,
consumerId,
sdk.NewDecCoinsFromCoins(validatorsRewardsTrunc...),
// allocate remaining rewards to the community pool
remainingRewards, remainingChanges := remaining.TruncateDecimal()
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 ICS rewards to community pool",
"consumerId", consumerId,
"chainId", chainId,
"error", err.Error(),
)
return types.ConsumerRewardsAllocation{}, err
}

// 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(),
insumity marked this conversation as resolved.
Show resolved Hide resolved
"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()),
),
)
return alloc, nil
}

// AllocateTokens performs rewards distribution to the community pool and validators
// based on the Partial Set Security distribution specification.
func (k Keeper) AllocateTokens(ctx sdk.Context) {
// return if there is no coins in the consumer rewards pool
if k.GetConsumerRewardsPool(ctx).IsZero() {
return
}

// allocate remaining rewards to the community pool
remainingRewards, remainingChanges := remaining.TruncateDecimal()
err = k.distributionKeeper.FundCommunityPool(context.Context(ctx), remainingRewards, k.accountKeeper.GetModuleAccount(ctx, types.ConsumerRewardsPool).GetAddress())
// Iterate over all launched consumer chains.
// To avoid large iterations over all the consumer IDs, iterate only over
// chains with an IBC client created.
for _, consumerId := range k.GetAllConsumersWithIBCClients(ctx) {
oldRewards := k.GetConsumerRewardsAllocation(ctx, consumerId)
returnedRewards, err := k.AllocateConsumerRewards(ctx, consumerId, oldRewards)
if err != nil {
k.Logger(ctx).Error(
"fail to allocate ICS rewards to community pool",
"consumerId", consumerId,
"chainId", chainId,
"fail to allocate rewards for consumer chain",
"consumer id", consumerId,
"error", err.Error(),
)
continue
} else {
k.SetConsumerRewardsAllocation(ctx, consumerId, returnedRewards)
}

// set consumer allocations to the remaining rewards decimals
alloc.Rewards = validatorsRewardsChange.Add(remainingChanges...)
k.SetConsumerRewardsAllocation(ctx, consumerId, alloc)
allAllowlistedDenoms := append(k.GetAllConsumerRewardDenoms(ctx), k.GetAllowlistedRewardDenoms(ctx, consumerId)...)
for _, denom := range allAllowlistedDenoms {
cachedCtx, writeCache := ctx.CacheContext()
consumerRewards := k.GetConsumerRewardsAllocationByDenom(cachedCtx, consumerId, denom)
allocatedRewards, err := k.AllocateConsumerRewards(cachedCtx, consumerId, consumerRewards)
insumity marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
k.Logger(ctx).Error(
"fail to allocate rewards for consumer chain",
"consumer id", consumerId,
"error", err.Error(),
)
continue
}
k.SetConsumerRewardsAllocationByDenom(cachedCtx, consumerId, denom, allocatedRewards)
// TODO: fix for the tests
_ = writeCache
//writeCache()
}

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(),
)
// 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.
rewardsAlloc := k.GetConsumerRewardsAllocation(ctx, consumerId)
remainingRewardsAlloc, err := k.AllocateConsumerRewards(ctx, consumerId, rewardsAlloc)

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()),
),
)
if err == nil {
k.SetConsumerRewardsAllocation(ctx, consumerId, remainingRewardsAlloc)
}
}
}

Expand Down
Loading
Loading