Skip to main content
Version: Next

Incentives Module

Module Overview

The Incentives module enables the incentivization of token holders who participate in specific aspects of the system. It allows governance to configure incentive pools for tokens. Token holders can bond their tokens to these pools to earn incentives, which may come from multiple sources, such as minted native tokens or external incentives. The module provides flexibility in determining the types of tokens that can be used as rewards for each pool, facilitating dual incentives.

Concepts

Incentive Pool

An "incentive pool" is a mechanism for encouraging token holders to participate in a protocol. Governance can establish a pool for a particular token, enabling users to bond their tokens and receive incentives. Each pool caters to a single bond token but can receive incentives from multiple denominations. Governance can specify a list of reward tokens when creating the pool, and additional reward tokens can be appended later. The pools are structured as follows:

message Pool {
cosmos.base.v1beta1.Coin bonded_token = 1 [(gogoproto.nullable) = false];
repeated PoolRewardToken rewards = 2 [(gogoproto.nullable) = false];
}

message PoolRewardToken {
string denom = 1;
string amount = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string global_index = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string weight = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

Here, bonded_token represents the denomination and total bonded amount of the bond token, while rewards is a list of permitted reward denominations with the following properties:

  • denom represents the denomination of the reward token.
  • amount represents the accumulated yet unpaid reward.
  • global_index is an index tracking accumulated rewards, used for calculating each user's share.
  • weight signifies the token's weight, currently used only for distributing mint module incentives. The weight of each reward token can be updated through governance proposals.

Rewards can be sent to any pool, provided the sent denominations are listed as reward tokens for that pool. The mint module's mint denom, the chain's primary native coin, can also be used to pay incentives to pools at the end of each mint epoch, based on their weight. Pools with a non-zero weight of the minted denomination and a non-zero bonded amount receive a portion of the mint incentives proportional to their weight. The total incentives sent to the Incentives module are controlled in the Mint module.

This dual incentivization strategy can incentivize LP holders of AMM pools. The incentive pool concept promotes the targeted incentivization of specific user behaviors, aligning token holders' interests with the protocol's success, and stimulating adoption and engagement.

When a reward is paid to a pool, the corresponding reward token is updated to reflect the new rewards. Specifically, the global_index and amount of the reward token are adjusted as follows:

  • The global_index is updated using the formula:
Ing=Iog+RB,I^g_n = I^g_o + \frac{R}{B},

IogI_o^g represents the global index prior to the operation, B refers to the total bonded amount of the pool, and RR is the newly accrued reward added to the pool. The resultant IngI_n^g is the updated global index of the token.

  • The amount is refreshed by adding the newly accrued reward to the existing value.

In essence, these updates ensure that the reward token mirrors the rewards accrued by the pool, which can then be distributed to the rightful users. Notably, when a reward token is instantiated for the first time, its global index is set at 11 and the amount at 00.

Bond

As noted earlier, a system may contain multiple incentive pools. When a user binds tokens to a pool, a bond object is generated with the following structure:

message Bond {
string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
cosmos.base.v1beta1.Coin token = 2 [(gogoproto.nullable) = false];
repeated BondRewardToken rewards = 3 [(gogoproto.nullable) = false];
}

message BondRewardToken {
string denom = 1;
string pending_amount = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
string user_index = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

The address field stores the user's address, while the token field records the bonded amount and its denomination. The rewards property maintains the user's state for each reward denomination of the pool. The pending_amount indicates the quantity of pending rewards, and the user_index is employed to calculate the user's share of the reward for the bond.

Whenever a user bonds or unbonds tokens, or claims rewards, the reward tokens are adjusted accordingly. For each reward token, the properties are updated as follows:

  • The pending_amount is modified using the formula:
Pn=Po+(IgIou)×Bo,P_{n} = P_{o} + (I^g - I^u_{o})\times B_o,

Here, PoP_o, IouI^u_o, and BoB_o represent the pending_amount, user_index, and the user's bonded amount prior to the operation, respectively. The resultant PnP_n is the refreshed pending_amount. This formula incorporates the global_index IgI^g of the reward token in the pool, ensuring that the user's rewards are proportional to their stake in the pool.

  • The user_index is updated to the global_index of the reward token in the pool.

Following these updates, for unbond and claimReward operations, the pending_amount is disbursed to the user and then reset to zero. It's key to note that distributing the reward also reduces the reward quantity in the pool data.

It's crucial to remember that if a new reward token is added to a pool, all bonds established prior to the addition would not immediately have the new reward token in their data. In such scenarios, Iou=1I_o^u = 1 and Po=0P_o = 0 are used in the computations, as the user was in the pool when the new token was added and therefore receives the initial index.

Unbonding

As outlined in the previous section, when a user opts to unbond an amount from a pool, the amount is deducted from their bonded sum and the rewards are updated and paid accordingly. However, the unbonded amount may not be immediately accessible to the user due to an UnbondingPeriod, during which the user must wait before claiming the unbonded amount. It's important to note that even though the amount isn't instantly transferred to the user, it is indeed withdrawn from the pool and is no longer eligible for rewards during the unbonding period. The unbonding period length, a parameter adjustable via governance, can also be set to zero. In this scenario, users can either choose to automatically receive the unbonded amount in the same message execution, or defer the claim to a separate message. Regardless of the unbonding period length, an unbonding request may create a record with the following properties:

message Unbonding {
uint64 id = 1;
google.protobuf.Timestamp completion_time = 2 [
(gogoproto.nullable) = false,
(gogoproto.stdtime) = true
];
string address = 3;
string treasury_address = 4;
cosmos.base.v1beta1.Coin amount = 5 [(gogoproto.nullable) = false];
bool auto_claim = 6;
}
  • id: Auto-generated identifier for the unbonding instance.
  • completion_time: The sum of the unbonding period and the block time of the unbond request.
  • address: User address of the bond.
  • treasury_address: Address for the amount to be paid after the unbonding period ends and the unbonding is claimed.
  • amount: Unbonding amount to be claimed.
  • auto_claim: Determines whether this unbonding should be automatically claimed when the unbonding period ends, or if it should be manually claimed by the user.

Auto-claim instances are added to a queue (sorted by completion-time) for automatic claiming once the unbonding period completes.

Users can also cancel unbondings at any time. This feature allows users to re-bond the entire amount or a portion of it back to the pool without waiting for the unbonding period to end. Any remaining amount in the unbonding will be claimable at the completion time.

Transitions

End Block

Users may not have immediate access to unbonded amounts at the unbond time. They must wait for the unbonding period to claim their tokens. However, users can opt for the auto-claim feature that automatically transfers tokens to the designated address upon the completion of the unbonding period.

To implement the auto-claim feature, a queue of unbondings (sorted by completion time) is maintained. At the end of each block, unbondings with a completion time less than the block time are iterated over and claimed, transferring the tokens to the selected address.

Message

MsgUpdateParams

This message can be used to update module parameters through governance.

message MsgUpdateParams {
string authority = 1;
Params params = 2 [(gogoproto.nullable) = false];
}
message MsgUpdateParamsResponse {}

MsgCreatePool

A new pool for a bond denomination can be created using this message, with a list of reward tokens. The reward tokens can have a weight, which is currently used for mint distribution.

message MsgCreatePool {
string authority = 1;
string bond_denom = 2;
repeated WeightedRewardToken reward_tokens = 3 [(gogoproto.nullable) = false];
}
message WeightedRewardToken {
string denom = 1;
// weight is used for reward portion for each pool, when a reward is
// accrued from dist module, it will be distributed to each pool according to the weights
string weight = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}
message MsgCreatePoolResponse {}

MsgUpdateRewardTokenWeight

This message enables the governance to adjust a reward token's weight in a pool.

message MsgUpdateRewardTokenWeight {
string authority = 1;
string bond_denom = 2;
WeightedRewardToken reward_token = 3 [(gogoproto.nullable) = false];
}

message MsgUpdateRewardTokenWeightResponse {}

MsgAddRewardTokenToPool

This message permits the addition of a new reward token to a pool via governance.

message MsgAddRewardTokenToPool {
string authority = 1;
string bond_denom = 2;
WeightedRewardToken reward_token = 3 [(gogoproto.nullable) = false];
}

message MsgAddRewardTokenToPoolResponse {}

MsgBond

This message allows users to bond tokens to a pool. The response includes the actual bond object. If a bond for the given denomination already exists, it's updated and the revised instance, containing the total bond and rewards, is returned.

message MsgBond {
string creator = 1;
cosmos.base.v1beta1.Coin amount = 2 [(gogoproto.nullable) = false];
}

message MsgBondResponse {
Bond bond = 1 [(gogoproto.nullable) = false];
}

CLI command:

pryzmd tx incentives bond [amount]

MsgClaimReward

This message enables users to claim their accumulated rewards for a specific bond denomination. The rewards are sent to the provided treasury address. The response includes the claimed reward coins.

message MsgClaimReward {
string creator = 1;
string bond_denom = 2;
string treasury = 3;
}

message MsgClaimRewardResponse {
repeated cosmos.base.v1beta1.Coin rewards = 1 [(gogoproto.nullable) = false];
}

CLI command:

pryzmd tx incentives claim-reward [bond-denom] [treasury]

MsgUnbond

This message allows users to unbond tokens from a pool. The unbonded amount is sent to the unbond_treasury and the accumulated rewards are sent to the reward_treasury. Setting auto_claim to true enables automatic claiming of the unbonded amount after the unbonding period. The response includes the unbonding instance, if applicable. If the unbonding period is zero and auto_claim is true, no unbonding is created and the amount is sent instantly. The rewards are also included in the response.

message MsgUnbond {
string creator = 1;
cosmos.base.v1beta1.Coin amount = 2 [(gogoproto.nullable) = false];
string unbond_treasury = 3;
string reward_treasury = 4;
bool auto_claim = 5;
}

message MsgUnbondResponse {
Unbonding unbonding = 1;
repeated cosmos.base.v1beta1.Coin rewards = 2 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

To execute the command via CLI, use:

pryzmd tx incentives unbond [amount] [unbond-treasury] [reward-treasury] [auto-claim]

MsgCancelUnbonding

If a user has an unbonding instance, they can send this message to re-bond some or all of the unbonding amount back to the pool. The response will contain the new or updated bond instance information.

message MsgCancelUnbonding {
string creator = 1;
uint64 unbonding_id = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}

message MsgCancelUnbondingResponse {
Bond bond = 1 [(gogoproto.nullable) = false];
}

To execute the command via CLI, use:

pryzmd tx incentives cancel-unbonding [unbonding-id] [amount]

MsgClaimUnbonding

Once the unbonding period has ended, users can use this message to claim the unbonded tokens. The response will contain the unbonded amount sent to the treasury of the bonding.

message MsgClaimUnbonding {
string creator = 1;
uint64 unbonding_id = 2;
}

message MsgClaimUnbondingResponse {
cosmos.base.v1beta1.Coin amount = 1 [(gogoproto.nullable) = false];
}

To execute the command via CLI, use:

pryzmd tx incentives claim-unbonding [unbonding-id]

MsgIncentivizePool

Any user can send rewards to the pools to encourage preferred behavior. This message allows for dual incentives, enabling other protocols to promote the use of their assets on PRYZM.

message MsgIncentivizePool {
string creator = 1;
string bond_denom = 2;
repeated cosmos.base.v1beta1.Coin amount = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

message MsgIncentivizePoolResponse {}

To execute the command via CLI, use:

pryzmd tx incentives incentivize-pool [bond-denom] [amount]

Query

Params

This query fetches the current module parameters.

// QueryParamsRequest is request type for the Query/Params RPC method.
message QueryParamsRequest {}

// QueryParamsResponse is response type for the Query/Params RPC method.
message QueryParamsResponse {
// params holds all the parameters of this module.
Params params = 1 [(gogoproto.nullable) = false];
}

To execute the command via CLI, use:

pryzmd query incentives params

Pool

This query fetches an incentive pool.

message QueryGetPoolRequest {
string bond_denom = 1;
}

message QueryGetPoolResponse {
Pool pool = 1 [(gogoproto.nullable) = false];
}

To execute the command via CLI, use:

pryzmd query incentives show-pool [bond-denom]

Pools

This query fetches a list of incentive pools.

message QueryAllPoolRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
}

message QueryAllPoolResponse {
repeated Pool pool = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

To list all pools, use the following CLI command:

pryzmd query incentives list-pool

Bond

To fetch a specific bond for an address and bond denom, use the following queries:

message QueryGetBondRequest {
string address = 1;
string bond_denom = 2;
}

message QueryGetBondResponse {
Bond bond = 1 [(gogoproto.nullable) = false];
}

The corresponding CLI command is:

pryzmd query incentives show-bond [address] [bond-denom]

Bonds

To fetch a list of all bonds, use the following queries. Note that the address field is optional.

message QueryAllBondRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

message QueryAllBondResponse {
repeated Bond bond = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

The corresponding CLI command is:

pryzmd query incentives list-bond

Unbonding

To fetch a specific unbonding, use the following queries:

message QueryGetUnbondingRequest {
uint64 id = 1;
}

message QueryGetUnbondingResponse {
Unbonding unbonding = 1 [(gogoproto.nullable) = false];
}

The corresponding CLI command is:

pryzmd query incentives show-unbonding [id]

Unbondings

To fetch a list of all unbondings, use the following query. Note that the address field is optional and allows filtering by address.

message QueryAllUnbondingRequest {
cosmos.base.query.v1beta1.PageRequest pagination = 1;
string address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"];
}

message QueryAllUnbondingResponse {
repeated Unbonding unbonding = 1 [(gogoproto.nullable) = false];
cosmos.base.query.v1beta1.PageResponse pagination = 2;
}

The corresponding CLI command is:

pryzmd query incentives list-unbonding

Events

EventSetBond

Emitted when a bond is added to the store.

message EventSetBond {
Bond bond = 1 [(gogoproto.nullable) = false];
}

EventRemoveBond

Emitted when a bond is removed from the store.

message EventRemoveBond {
string address = 1;
string denom = 2;
}

EventSetUnbonding

Emitted when an unbonding is added to the store.

message EventSetUnbonding {
Unbonding unbonding = 1 [(gogoproto.nullable) = false];
}

EventRemoveUnbonding

Emitted when an unbonding is removed from the store.

message EventRemoveUnbonding {
uint64 id = 1;
}

EventSetPool

Emitted when a pool is added to the store.

message EventSetPool {
Pool pool = 1 [(gogoproto.nullable) = false];
}

EventSetParams

Emitted when the module parameters are updated.

message EventSetParams {
Params params = 1 [(gogoproto.nullable) = false];
}

EventClaimReward

Emitted when a user claims a bond reward.

message EventClaimReward {
string address = 1;
string bond_denom = 2;
string treasury = 3;
repeated cosmos.base.v1beta1.Coin rewards = 4 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

EventBond

This event triggers when a user bonds tokens to a pool.

message EventBond {
string address = 1;
cosmos.base.v1beta1.Coin amount = 2 [(gogoproto.nullable) = false];
}

EventUnbond

This event triggers when a user unbonds tokens from a pool.

message EventUnbond {
string address = 1;
cosmos.base.v1beta1.Coin amount = 2 [(gogoproto.nullable) = false];
repeated cosmos.base.v1beta1.Coin rewards = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
Unbonding unbonding = 4 [(gogoproto.nullable) = true];
string reward_treasury = 5;
string unbond_treasury = 6;
}

EventClaimUnbonding

This event triggers when a user claims an unbonding.

message EventClaimUnbonding {
uint64 id = 1;
}

EventCancelUnbonding

This event triggers when a user cancels an amount of an unbonding.

message EventCancelUnbonding {
uint64 id = 1;
cosmos.base.v1beta1.Coin amount = 2 [(gogoproto.nullable) = false];
}

EventIncentivizePool

This event triggers when a pool is incentivized, either by user or minted tokens.

message EventIncentivizePool {
string bond_denom = 1;
repeated cosmos.base.v1beta1.Coin amount = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}

Listeners

MintHooks

The incentives module leverages mint hooks to monitor the distribution of new tokens by the mint module. It primarily calls the OnPoolIncentivesRewardsDistributed method to allocate rewards to its pools.

Upon invoking this hook, the incentives module retrieves a list of its pools and selects those with a positive bonded amount and a positive weight for the mintDenom reward token. It then distributes the minted tokens proportionate to these pools' weights. Any residual tokens left after the distribution are transferred to the fee-collector account for further distribution.

State

Parameters

Parameters are stored with this store key:

(
[]byte("v1/params/")
) -> ProtoBuf(Params)

Pools

Bonds are stored with their bond denomination with this store key:

(
[]byte("v1/pool/") |
[]byte(bondDenom)
) -> ProtoBuf(Pool)

Bonds

Bonds are stored using their bond denomination and address, as follows:

(
[]byte("v1/bond/") |
[]byte(lengthPrefixedAddress) |
[]byte(bondDenom) |
) -> ProtoBuf(Bond)

Unbonding

Unbondings are stored using their id, as follows:

(
[]byte("v1/unbonding/") |
[]byte(id)
) -> ProtoBuf(Unbonding)

The uint64 id of unbondings, starting from 0, increments each time a new unbonding is created. This incrementing id is also stored in the context as:

(
[]byte("v1/unbonding-count/")
) -> []byte(count)

In order to allow fetching unbondings for a specific address, a secondary index is maintained as follows:

(
[]byte("v1/address-unbonding/") |
[]byte(lengthPrefixedAddress) |
[]byte(id)
) -> []byte(id)

Furthermore, a queue of auto-unbondings sorted by completion time is maintained as follows:

(
[]byte("v1/auto-unbonding/") |
[]byte(completionTime) |
[]byte(id)
) -> []byte(id)

Parameters

This module's only parameter is the unbonding period:

message Params {
google.protobuf.Duration unbonding_period = 1 [
(gogoproto.nullable) = false,
(gogoproto.stdduration) = true
];
}

Genesis

The genesis state of this module includes the parameters, a list of pools, bonds, and unbondings, and the total count of unbondings created to date.

// GenesisState defines the incentives module's genesis state.
message GenesisState {
Params params = 1 [(gogoproto.nullable) = false];
repeated Pool pool_list = 2 [(gogoproto.nullable) = false];
repeated Bond bond_list = 3 [(gogoproto.nullable) = false];
repeated Unbonding unbonding_list = 4 [(gogoproto.nullable) = false];
uint64 unbonding_count = 5;
// this line is used by starport scaffolding # genesis/proto/state
}