Skip to main content
Version: 0.9.0

AMM Module

Module Overview

The AMM module equips the protocol with support for various types of automated market makers (AMMs). It allows the creation, administration, and interaction with pools. All pools are centrally managed within a single entity known as the vault. This vault oversees the pools, their tokens, and their balances. It also offers gas-efficient features, such as batch swaps, to facilitate easy arbitrage and efficient pool interaction.

The AMM module also empowers users to place long-term trade orders with price limits. These orders can be executed incrementally, offering time-weighted pricing with minimized price impacts. Additionally, the AMM module incorporates an order matching system. Here, proposers can suggest matching a set of orders against the AMM's current price.

Collectively, these features provide an efficient framework for token trading on the Pryzm protocol, with specific AMM types designed for yield trading.

Concepts

Pools

Liquidity pools serve as the core building block of the AMM module. Despite the module supporting various pool types, all pools share certain common data and interfaces within the module. The following data is stored in the context for all pools:

message Pool {
uint64 id = 1;
string name = 2;
// this is the constant swap fee ratio, for dynamic swap fees other pools might have other parameters.
// for example, check yamm configuration
string swap_fee_ratio = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
PoolType pool_type = 4;
string creator = 5 [(cosmos_proto.scalar) = "cosmos.AddressString"];
bool recovery_mode = 6;
bool paused_by_gov = 7;
bool paused_by_owner = 8;
PoolPauseWindow owner_pause_window_timing = 9 [(gogoproto.nullable) = true];
// if protocol fee parameters are nil, then the values are read from treasury module parameters
string swap_protocol_fee_ratio = 10 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
string join_exit_protocol_fee_ratio = 11 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
// if not empty, only these addresses can initialize the pool
repeated string initialization_allow_list = 12;
// these have the same permissions as the creator, except for
// updating the admins list and pausing the pool
repeated string admins = 13;
repeated string pause_allow_list = 14;
}

// PoolType enumerates the valid types for pool_type.
enum PoolType {
option (gogoproto.goproto_enum_prefix) = false;
POOL_TYPE_WEIGHTED = 0 [(gogoproto.enumvalue_customname) = "WeightedPoolType"];
POOL_TYPE_YAMM = 1 [(gogoproto.enumvalue_customname) = "YammPoolType"];
}

message PoolPauseWindow {
int64 pause_window_end_unix_millis = 1;
int64 buffer_period_end_unix_millis = 2;
}
  • id denotes an auto-generated identifier for pools, beginning from 0.
  • name represents the pool's name, utilized in the denomination of its LP tokens.
  • swap_fee_ratio, as implied, is the fee ratio imposed on pool swaps.
  • pool_type indicates the pool's type, dictating its operational procedures and used in crafting the pool implementation for interaction purposes.
  • creator is the address granted the authority to control the pool and modify parameters like the swap fee ratio.
  • recovery_mode signifies whether a pool is in recovery mode, a topic we will delve into in detail later.
  • paused_by_gov reveals if the pool has been paused by governance.
  • paused_by_owner is true if the creator has paused the pool.
  • owner_pause_window_timing displays the timing window during which the pool's owner can pause it, as well as the duration the pool can stay paused.
  • swap_protocol_fee_ratio is the protocol fee levied on swap operations. If this property is null, the effective ratio is sourced from module parameters.
  • join_exit_protocol_fee_ratio is the protocol fee applied to join and exit operations. If this property is null, the effective ratio is sourced from module parameters.
  • initialization_allow_list, when set, restricts pool initialization to the given addresses only.
  • admins is a list of addresses allowed to do anything the creator can, except for updating the admins list and pausing the pool.
  • pause_allow_list is a list of addresses allowed to pause the pool, but cannot unpause the pool.

In addition to these properties, all pool types possess a token set, with the following common data:

message PoolToken {
uint64 pool_id = 1;
string denom = 2;
string balance = 3 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
CircuitBreaker circuit_breaker = 4 [(gogoproto.nullable) = true];
}

message CircuitBreaker {
string reference_lpt_price = 1 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string lower_bound = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string upper_bound = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string reference_normalized_weight = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string adjusted_upper_bound = 5 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string adjusted_lower_bound = 6 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

Each pool token includes pool_id, denomination, and pool balance. The circuit_breaker property mitigates drastic price fluctuations in the pool.

All liquidity pools should leverage a common data structure and implement an API as defined in the amm module. This API facilitates interaction with all pools, irrespective of their underlying logic. The API is as follows:

type PoolApi interface {
GetData() *types.Pool
GetSwapFeeRatio(ctx sdk.Context, tokenIn, tokenOut types.PoolToken) (sdk.Dec, error)
GetJoinExitSwapFeeRatio(ctx sdk.Context) (sdk.Dec, error)
SwapGivenIn(ctx sdk.Context, tokenIn types.TokenAmount, tokenOut types.PoolToken) (amountIn sdkmath.Int, protocolFee sdkmath.Int, err error)
SwapGivenOut(ctx sdk.Context, tokenIn types.PoolToken, tokenOut types.TokenAmount) (amountOut sdkmath.Int, protocolFee sdkmath.Int, err error)
AfterSwap(ctx sdk.Context, summary types.SwapSummary, allowZeroOutput bool) error

ComputeSpotPrice(ctx sdk.Context, tokenIn types.PoolToken, tokenOut types.PoolToken, applyFee bool) (sdk.Dec, error)

// ComputeLptPriceInTokenTerms calculates price of lpToken in terms of token
ComputeLptPriceInTokenTerms(ctx sdk.Context, token types.PoolToken, lptSupply sdkmath.Int) (sdk.Dec, error)

// InitializePool does initialization computations given exact amount for each token, amounts should all be positive
// tokens array must include all tokens in the pool and should be sorted by denom as stored in kvStore
InitializePool(ctx sdk.Context, tokensSortedByDenom []types.TokenAmount) (lpOut sdkmath.Int, protocolFees []sdkmath.Int, err error)
AfterInitialization(ctx sdk.Context, tokenAmounts []types.TokenAmount, lptOut sdkmath.Int) error

// JoinGivenExactTokensIn does join given exact amount for each token, some token amounts might be zero
// tokens array must include all tokens in the pool and should be sorted by denom as stored in kvStore
JoinGivenExactTokensIn(ctx sdk.Context, tokensSortedByDenom []types.TokenAmount, lptSupply sdkmath.Int) (lpOut sdkmath.Int,
protocolFees []sdkmath.Int, err error)
JoinTokenGivenExactLptOut(ctx sdk.Context, token types.PoolToken, lpToken types.TokenAmount) (amountIn sdkmath.Int, protocolFee sdkmath.Int,
err error)
JoinAllTokensGivenExactLptOut(ctx sdk.Context, tokens []types.PoolToken, lpToken types.TokenAmount) (amountsIn []sdkmath.Int,
protocolFees []sdkmath.Int, err error)
AfterJoinNonInitJoin(ctx sdk.Context, summary types.JoinSummary, allowZeroOutput bool) error

// ExitGivenExactTokensOut does exit given exact amount for each token, some token amounts might be zero
// tokens array must include all tokens in the pool and should be sorted by denom as stored in kvStore
ExitGivenExactTokensOut(ctx sdk.Context, lptSupply sdkmath.Int, tokens []types.TokenAmount) (lpIn sdkmath.Int,
protocolFee sdkmath.Int, err error)
ExitTokenGivenExactLptIn(ctx sdk.Context, lpToken types.TokenAmount, token types.PoolToken) (amountOut sdkmath.Int,
protocolFee sdkmath.Int, err error)
ExitAllTokensGivenExactLptIn(ctx sdk.Context, lpToken types.TokenAmount, tokens []types.PoolToken) (amountsOut []sdkmath.Int,
protocolFee sdkmath.Int, err error)
RecoveryModeExit(ctx sdk.Context, lpToken types.TokenAmount, tokens []types.PoolToken) (amountsOut []sdkmath.Int, err error)

AfterExit(ctx sdk.Context, summary types.ExitSummary, allowZeroOutput bool) error

GetVirtualBalance(ctx sdk.Context, token types.PoolToken) (sdk.Dec, error)

// ApplyCircuitBreakerSettingsOnTokens applies circuit breaker settings on the tokens given in the array
// note that these tokens might not be all the tokens of the pool, tokens and breakers should be for the same array of tokens in the same order
// this method does not store the tokens, just applies changes on the given parameters
ApplyCircuitBreakerSettingsOnTokens(ctx sdk.Context, tokens []types.PoolToken, breakers []types.TokenCircuitBreakerSettings) ([]types.PoolToken, error)
}
  1. GetData() *types.Pool Returns a pointer to the current state of the pool.

  2. GetSwapFeeRatio(ctx sdk.Context, tokenIn, tokenOut types.PoolToken) (sdk.Dec, error) Returns the swap fee ratio for the given input and output tokens, where the swap fee ratio is the ratio of the swap fee to the total input token amount. The swap fee may vary dynamically depending on the tokens being swapped.

  3. GetJoinExitSwapFeeRatio(ctx sdk.Context) (sdk.Dec, error) Returns the join/exit swap fee ratio.

This method calculates the fee ratio for swaps during join and exit operations. In non-proportional join and exit operations, underlying swaps may need to be computed in addition to liquidity provision or withdrawal operations. Even if the pool has a dynamic swap fee, the join and exit operation fees do not depend on the tokens being swapped.

  1. SwapGivenIn(ctx sdk.Context, tokenIn types.TokenAmount, tokenOut types.PoolToken) (amountIn sdkmath.Int, protocolFee sdkmath.Int, err error)

    This method calculates a swap operation using the input token amount and the output token. It returns the amount of the input token swapped, the protocol fee, and any errors that occurred during the operation.

  2. SwapGivenOut(ctx sdk.Context, tokenIn types.PoolToken, tokenOut types.TokenAmount) (amountOut sdkmath.Int, protocolFee sdkmath.Int, err error)

    This method calculates a swap operation using the output token amount and the input token. It returns the amount of the output token swapped, the protocol fee, and any errors that occurred during the operation.

  3. AfterSwap(ctx sdk.Context, summary types.SwapSummary, allowZeroOutput bool) error

    This method performs any operations after a swap, such as checking the swapped amounts for circuit breaking logic to prevent unwanted swaps. It takes a SwapSummary struct, which contains information about the swap, and a boolean flag allowZeroOutput indicating if zero output is allowed in the swap.

  4. ComputeSpotPrice(ctx sdk.Context, tokenIn types.PoolToken, tokenOut types.PoolToken, applyFee bool) (sdk.Dec, error)

    This method calculates the spot price of the input token relative to the output token. The applyFee parameter specifies whether to include the swap fee in the spot price calculation.

  5. ComputeLptPriceInTokenTerms(ctx sdk.Context, token types.PoolToken, lptSupply sdkmath.Int) (sdk.Dec, error)

    This method calculates the spot price of the liquidity pool token (LPT) relative to a given token. The lptSupply parameter is the total supply of LPTs.

  6. InitializePool(ctx sdk.Context, tokensSortedByDenom []types.TokenAmount) (lpOut sdkmath.Int, protocolFees []sdkmath.Int, err error)

    This method initializes the pool, computing the minted LP tokens for the provided liquidity. The tokensSortedByDenom parameter is an array of TokenAmount structs sorted by token denomination as stored in the key-value store. The method returns the number of LPTs to be minted, the protocol fees charged for the initialization, and any errors that occurred during the operation.

  7. AfterInitialization(ctx sdk.Context, tokenAmounts []types.TokenAmount, lptOut sdkmath.Int) error

    This method performs any operations after initialization, such as validating the pool state and applying circuit breaking logic. It takes an array of TokenAmount structs, which contain the initial token amounts and the amount of minted LPTs.

  8. JoinGivenExactTokensIn(ctx sdk.Context, tokensSortedByDenom []types.TokenAmount, lptSupply sdkmath.Int) (lpOut sdkmath.Int, protocolFees []sdkmath.Int, err error)

    This method calculates LP tokens for adding liquidity to the pool given the exact token amounts. The tokensSortedByDenom parameter is an array of TokenAmount structs sorted by token denomination as stored in the key-value store. The lptSupply parameter is the total supply of LPTs. The method returns the number of LPTs minted, the protocol fees charged for the join operation, and any errors that occurred during the operation.

  9. JoinTokenGivenExactLptOut(ctx sdk.Context, token types.PoolToken, lpToken types.TokenAmount) (amountIn sdkmath.Int, protocolFee sdkmath.Int, err error)

    This function calculates the required liquidity for a specific token to obtain a certain amount of LP tokens. It returns the quantity of the input token needed in the pool, the protocol fee for the join operation, and any errors that occur.

  10. JoinAllTokensGivenExactLptOut(ctx sdk.Context, tokens []types.PoolToken, lpToken types.TokenAmount) (amountsIn[]sdkmath.Int, protocolFees []sdkmath.Int, err error)

    This function computes a proportional join operation using a list of all tokens and the necessary LP tokens to be minted. It returns an array of input token amounts needed in the pool, an array of protocol fees for the join operation, and any errors that occur.

  11. AfterJoinNonInitJoin(ctx sdk.Context, summary types.JoinSummary, allowZeroOutput bool) error

    This function performs post-join operations, such as circuit breaking logic. It requires a JoinSummary struct containing information about the join, and a allowZeroOutput boolean flag indicating if zero output is acceptable in the join.

  12. ExitGivenExactTokensOut(ctx sdk.Context, lptSupply sdkmath.Int, tokens []types.TokenAmount) (lpIn sdkmath.Int, protocolFee sdkmath.Int, err error)

    This function calculates an exit operation using the exact token amounts to be withdrawn. The tokens parameter is an array of TokenAmount structs sorted by token denomination in the key-value store. The lptSupply parameter represents the total LP tokens supply. It returns the amount of LPTs to be burned, the protocol fee for the exit operation, and any errors that occur.

  13. ExitTokenGivenExactLptIn(ctx sdk.Context, lpToken types.TokenAmount, token types.PoolToken) (amountOut sdkmath.Int, protocolFee sdkmath.Int, err error)

    This function calculates the removal of liquidity from the pool given the exact amount of LPTs to be burned and the output token. It returns the amount of output token withdrawn from the pool, the protocol fee for the exit operation, and any errors that occur.

  14. ExitAllTokensGivenExactLptIn(ctx sdk.Context, lpToken types.TokenAmount, tokens []types.PoolToken) (amountsOut []sdkmath.Int, protocolFee sdkmath.Int, err error)

    This function calculates the removal of liquidity from the pool given the exact amount of LPTs to be burned and all output tokens. The tokens parameter is an array of PoolToken structs. It returns an array of output token amounts withdrawn from the pool, the protocol fee for the exit operation, and any errors that occur.

  15. RecoveryModeExit(ctx sdk.Context, lpToken types.TokenAmount, tokens []types.PoolToken) (amountsOut []sdkmath.Int, err error)

    In recovery mode, this function enables an exit operation that charges no protocol fees and executes proportional exits with minimal complexity. It calculates the removal of liquidity from the pool given the exact amount of LPTs to be burned and all output tokens. The tokens parameter is an array of PoolToken structs. It returns an array of output token amounts withdrawn from the pool and any errors that occur.

  16. AfterExit(ctx sdk.Context, summary types.ExitSummary, allowZeroOutput bool) error

    This function performs post-exit operations, such as checking circuit breakers. It requires an ExitSummary struct containing information about the exit, and a allowZeroOutput boolean flag indicating if zero output is acceptable in the exit.

  17. GetVirtualBalance(ctx sdk.Context, token types.PoolToken) (sdk.Dec, error) This method returns the virtual balance of a given token. Virtual balances are utilized in some pools during the addition or withdrawal of tokens, creating arbitrage opportunities, which will be further discussed.

  18. ApplyCircuitBreakerSettingsOnTokens(ctx sdk.Context, tokens []types.PoolToken, breakers []types.TokenCircuitBreakerSettings) ([]types.PoolToken, error)

    This method applies circuit breaker settings to the provided tokens. The tokens parameter is an array of PoolToken structs, whereas the breakers parameter is an array of TokenCircuitBreakerSettings structs. The method returns an array of PoolToken structs with updated circuit breaker settings and any errors that occurred during the operation. Note that these tokens may not represent all tokens in the pool, hence tokens and breakers should correspond to the same array of tokens in the same order. This method does not store tokens, it simply applies changes to the provided parameters.

This pool API is used by all types of pools to perform calculations. Note that the initialization, swap, join, and exit methods in this API are not responsible for altering the stored balance of the tokens or performing token transfers. These methods are solely for calculating how the operation would modify the pool, with the remaining operations conducted by the amm module in the vault.

The Vault

As stated earlier, our system can contain numerous types of pools, each in multiple instances. The amm module, however, offers a unified way to interact with all these pools. This unity is achieved through a concept known as the Vault. The Vault contains all tokens from all liquidity pools, and maintains a record of these pools' liquidity shares. To clarify, we do not have a separate account for each pool. Instead, we have a single bank account, known as the Vault account, where all liquidity is stored.

Housing all balances in a single account has advantages such as creating a large liquidity pool, thereby facilitating flash loans or other features. In contrast to creating separate accounts and managing them, this approach reduces complexity when establishing a new pool. One of the key benefits of the Vault is its ability to provide a batch swap feature, which will be thoroughly explored.

For a deeper understanding of the Vault's operation, let's examine the steps involved in executing a swap request in the amm module. A swap request contains the following properties:

message Swap {
uint64 pool_id = 1;
string amount = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
SwapType swap_type = 3;
string token_in = 4;
string token_out = 5;
}

enum SwapType {
option (gogoproto.goproto_enum_prefix) = false;
SWAP_TYPE_GIVEN_IN = 0 [(gogoproto.enumvalue_customname) = "SWAP_GIVEN_IN"];
SWAP_TYPE_GIVEN_OUT = 1 [(gogoproto.enumvalue_customname) = "SWAP_GIVEN_OUT"];
}

In addition to swap details, we also have the address from which the input tokens are drawn and the address to which the output tokens are sent. The Vault also allows for input and output amount limits to prevent users from receiving undesired pricing. Note that the swap request only includes the pool_id and does not consider the type of pool or how the pool will execute the operation. With these properties in mind, the swap operation is executed as follows:

  1. Verify pool initialization
  2. Assign custom gas amount for operation
  3. Confirm the vault isn't paused
  4. Validate swap request
  5. Load pool state and establish logic API based on pool type
  6. Confirm the pool isn't paused
  7. Load input and output pool tokens
  8. Use the pool API method to calculate the swapped amounts:
    1. If the swap type is SWAP_GIVEN_IN, call pool.SwapGivenIn
    2. If the swap type is SWAP_GIVEN_OUT, call pool.SwapGivenOut
  9. Retrieve input/output amounts from previous step
  10. Increment input token balance in store
  11. Decrement output token balance in store
  12. Invoke pool.AfterSwap to allow the pool to verify its state post-operation
  13. Emit swap event
  14. Transfer input and output amounts to/from designated address

Note that steps 3-13 execute the swap operation without transferring any tokens in the bank module. Once these steps are complete, we execute the bank methods to transfer tokens. This approach allows the vault to support batch swaps and flash swaps.

Batch Swap

Consider a system with two pools, one with tokens xx and yy, and another with tokens yy and zz. If a user wants to swap some xx for zz, they would need to execute two separate transactions:

  1. Swap xx for yy in the first pool
  2. Swap the acquired yy for zz in the second pool

Besides gas inefficiency and poor user experience, this approach poses a significant problem. What happens if the second trade fails, and the price in the second pool has fluctuated too much, leaving the user with less zz than expected? The user cannot reverse the entire process and retrieve their initial xx tokens!

The AMM vault offers a feature called batch swaps. A batch swap is a group of swap operations, all executed at once. If one step fails, the entire process is reversed, allowing the user to recover their initial tokens. A batch swap request includes a series of swap steps, each structured as follows:

message SwapStep {
uint64 pool_id = 1;
string amount = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = true
];
string token_in = 4;
string token_out = 5;
}

Note that this structure lacks a SwapType, which must be provided once for the whole operation in a separate parameter. Therefore, all step types are identical. Another point of interest is that the amount property is nullable, allowing users to automatically use the computed amount of one step as the amount for the next step. For example, when trading xx for zz, the input of the second step should equal the output of the first one, so we can leave that amount as null. The overall procedure for executing a batch swap is as follows:

amountsDiff = make(map[string]sdkmath.Int)

// computedAmount of a step can be used as amount of the next step
var computedAmount types.TokenAmount
for i, step := range steps {
// computedAmount of a step can be used as amount of the next step
if step.Amount == nil {
if i == 0 {
return data, sdkerrors.Wrapf(types.ErrInvalidSwap, "first step must have amount")
}
if step.GetGivenToken(swapType) != computedAmount.Token.Denom {
return data,
sdkerrors.Wrapf(types.ErrInvalidSwap, "amount should not be nil when given amount of step is not equal to computed of previous step")
}
step.Amount = &computedAmount.Amount
}

computedAmount = // execute the step
// increase the amountsDiff for tokenIn
// decrease the amoutns diff for tokenOut
....
}

for denom, diff := range amountsDiff {
if diff.IsZero() {
continue
}
if diff.IsNegative() {
// send amount from the vault to user
} else if diff.IsPositive() {
// send amount form user to the vault
}
}

The swap execution is first conducted within the vault to determine the minimal transfer needed between the vault and the address. This is an advantage of using a vault instead of managing balances in the bank module, as it permits unique functions such as arbitrage without requiring any tokens in your account. For instance, if a user identifies a path to start with 10usdt10usdt and end with 15usdt15usdt, this series of swaps can be submitted in a batch swap, allowing the user to gain 5usdt5usdt without paying any tokens.

Weighted AMM

We have discussed the fundamental concepts of pools and vaults so far. This section introduces a specific type of pool implemented in the AMM module, known as a weighted pool. This pool employs a constant product equation.

Weighted pools facilitate special operations like updating token weights, introducing tokens, and removing tokens. These pools and their equations are detailed in the Weighted AMM document.

Yield AMM

Another type of AMM we have established in the AMM module is a form of weighted pools specifically designed for yield trading. These pools enable users to trade cASSET, pASSET, and yASSET of a specific asset per pool. The design of yield AMM is elaborated in the Yield AMM page.

Nested Pool

Nested pools are decentralized liquidity pools that use LP (liquidity provider) tokens from one pool as liquidity in another. They are designed to interconnect a series of pools, creating a network of linked liquidity. Nested pools allow LPs to earn yield from multiple pools simultaneously without managing their liquidity individually, leading to increased efficiency and profitability for them, and enhanced liquidity and trading activity for the interconnected pools. Nested pools are a novel solution for constructing decentralized liquidity networks and facilitating seamless asset exchange in the DeFi ecosystem.

As per this definition, we don't need a specific pool type to support nesting; we can simply use the weighted pool type to create a new weighted pool with LP tokens as liquidity. A use case of these pools in PRYZM is a pool holding LP tokens of YAMM pools. For instance, if we have atom and luna as refractable assets in PRYZM, we can create a YAMM pool for each of these, and then create a weighted pool with the LP of these two pools, along with other tokens like PRYZM's native token or USDC tokens.

Circuit Breakers

Circuit breakers are a crucial mechanism in the AMM that help prevent extreme token price movements in a pool. They ensure that token prices stay within defined limits by verifying that a token's LPT price post-operation doesn't exceed a pre-set range.

The circuit breaker operates using two parameters, a boundary and a reference LPT price, that can be set for each token in the pool. The LPT price for a pool token is calculated as follows:

LPT(tokeni)=LPTsupply×wiBiLPT(token_i) = \frac{LPT_{supply} \times w_i}{B_i}

Where supply represents the circulating supply of LP tokens.

Let's envision a function that calculates the circuit breaker boundaries based on the defined parameters:

lower,upper=CB(wi,bl,bu,LPTref)lower, upper = CB(w_i, b_l, b_u, LPT_{ref})

Here, wiw_i is the token's weight, LPTrefLPT_{ref} is the reference LPT price, blb_l is the lower boundary parameter, and bub_u is the upper boundary parameter. Note: wiw_i is the current weight of the token (at the time of boundary checking), while LPTref,bl,buLPT_{ref}, b_l, b_u are previously set parameters via governance or pool ownership.

Let's assume that we've calculated lower,upperlower, upper values using the CB(.)CB(.) function. The circuit breaker logic then validates the following criteria:

lowerLPT(tokeni)upperlower \leq LPT(token_i) \leq upper

If the operation (e.g., swap) fails to meet this criterion, it will not proceed. Note: rounding operations for calculating the LPT depend on the boundary being checked—rounding down for the lower bound and up for the upper bound.

Bounds computation: We'll now elucidate the equations for the CBCB function. The computations for both upper and lower boundaries are identical except for the power and multiply functions—rounding up for lower bounds and down for upper bounds. If the boundary parameter is bb (either blb_l or bub_u), the output is calculated as:

BoundRatio=b(1wi)bound=BoundRatio×LPTrefBoundRatio = b^{(1 - w_i)} \\ bound = BoundRatio \times LPT_{ref}

Since the pool allows weight updates, these computations should be performed if the weights have changed since the circuit breaker parameters were set. Note: the boundary parameter bb is further adjusted relative to the weight. As the weight increases, the exponent approaches 00, bringing the boundary ratio closer to 11. This adjustment results in a tighter boundary, closer to the reference LPT, making the constraints stricter for higher-weighted tokens.

Each pool operation affects prices differently, so we must check certain circuit breakers for each operation. These checks are always performed after the operation in the AfterSwap, AfterInitialization, AfterJoinNonInitJoin, and AfterExit processes.

For each pool, the boundaries are checked as follows:

  • After swap: The lower boundary is checked for the token-in and the upper for the token-out.
  • After initialization: All circuit breakers are checked in both directions.
  • After exit: All circuit breakers are checked, unless it's a recovery-mode exit or a proportional exit.
  • After join: All circuit breakers are checked, unless it's a proportional join.

Pause Mode

Pool Pause Mode

The AMM's pause mode feature allows pool owners and governance to temporarily disable various pool functionalities in the event of an emergency or error. Once a pool is created, the owner has the ability to pause the pool during a specific time window. If the pool is paused within this window, it can be either unpaused or re-paused by the owner. However, if the pool remains paused beyond the initial window and the owner doesn't unpause it, it will be automatically unpaused after the pause and buffer windows have passed. The pool's pause window timing and pause state are included in the pool data structure, as outlined in the pools section.

In addition to the owner, governance can also initiate pause mode for a pool. If governance decides to pause a pool, time windows become irrelevant and the pool remains in pause mode until governance decides to unpause it.

During pause mode, the following pool functionalities are disabled:

  • Updating swap fees
  • Updating weights of weighted pools
  • Swaps
  • Join and Exit operations (excluding recoveryExit)
  • Pool initialization
  • Token introduction (including pASSETs for YAMM pools)
  • Token removal (including pASSETs for YAMM pools)
  • Setting circuit breakers

However, please note that the recoveryExit method remains available during pause mode, enabling liquidity providers to retrieve their assets in emergencies.

Vault Pause Mode

Governance can also pause the entire AMM vault. If the vault enters pause mode, the following functionalities are disabled:

  • Join/init pool
  • Register pool
  • Token removal
  • Token introduction
  • Swap
  • Batch swap
  • Submit, execute, match orders

In pause mode, the vault cannot process new transactions. As the vault is built into the chain and not contract-based, there is no set end for the pause window. However, as the governance of the Cosmos chain can update the entire chain's source code, it has the authority to pause the vault whenever necessary to protect funds.

Recovery Mode

The AMM's recovery mode feature is a crucial safety measure designed to help liquidity providers recover from critical situations. When a pool enters a non-functional state, recovery mode can be activated by governance proposals, providing a safe exit from any pool during an emergency. Unlike pause mode, recovery mode is always reversible and does not permanently disable the pool. Even if a pool is in pause mode, governance can switch it to recovery mode, enabling the recoveryExit method for liquidity providers to retrieve their assets.

Activation of recovery mode requires a flag in the pool models, which can only be triggered through governance. Since recovery mode deactivates protocol fees, it cannot be activated by pool owners. When activated, pools skip protocol fee computations in every function that charges protocol fees on top of swap fees.

Each pool should implement a RecoveryModeExit function, performing proportional exits without charging fees or complex computations like computing virtual balances. While yASSET trades always have protocol fees, they will not be disabled even in recovery mode, as they are necessary to overcome rounding errors. Also, order scheduling and matching fees are not disabled during recovery mode, as they are charged for additional features rather than AMM internal features. Overall, recovery mode provides a vital safety net for liquidity providers, allowing them to recover from critical situations without incurring additional fees or complex computations.

Pulse Trades

The Pulse trade system, also known as the order system, minimizes the price impact of high-volume trades. It allows users to submit long-term orders to be executed gradually, reducing the trade's overall price impact. To enhance user experience and provide flexible fund management, the system only locks the necessary funds for executing one step of the order at a time, with remaining funds locked on-demand as the order progresses. The order system also contains a feature that enhances performance by permitting off-chain solvers to propose order matches. These matches can be executed instantly, allowing orders to be processed efficiently without delays. Solvers are rewarded with a fee for their contribution. Overall, the order system is a robust tool for traders aiming to execute high-volume trades, minimizing price impact and maximizing efficiency. The Pulse Trade page contains a detailed explanation of the system.

Transitions

Begin Block

The Amm module carries out the following key operations at its begin blockers:

Check YAMM Pool Tokens

To manage tokens in YAMM pools, we cycle through all tokens to manage their introduction and removal as follows:

  • Add expiring tokens to the expiring queue if they're in the expiration interval and no expiring/introducing virtual balance is registered for them.
  • Delete tokens with zero balance and no introducing virtual balance.

Check Virtual Pool Tokens

The second operation involves checking virtual pool tokens. The module verifies if the introducing interval has passed. If so, the module removes the virtual balance from the token. This process is essential to clear the virtual balance tokens from the module's storage.

Please note, if token removal is underway, the virtual balance remains active even if the time interval has passed, meaning the virtual balance continues to increase until the token balance is completely drained.

End Block

The end-blocker is vital for managing pulse-trade orders. As previously described in the pulse trade section, there are two separate queues of orders: the schedule queue and the execution queue. During each block, the end-blocker first cycles through the schedule queue to add relevant orders to the execution queue. To ensure efficient processing and avoid overload, the end-blocker uses special indexing and sorting techniques as outlined in the pulse-trade section, and limits the number of orders processed from the schedule queue per block using the MaxSchedulePerBlock parameter.

The second crucial task performed by the end-blocker is executing appropriate orders based on their price limits. Like the scheduling process, the end-blocker uses indexing and specific methods to execute orders, as described in the pulse-trade section. To prevent chain-breaking due to an excessive number of orders, the end-blocker uses the MaxOrdersPerBlock parameter to limit the number of orders executed per block.

Message

For more information about the supported messages, please visit the messages page.

Query

For more information about the supported queries, please visit the queries page.

Events

For more information about the emitted events, please visit the events page.

Listeners

MaturityLevelListener

The Amm module implements the maturity-level listener provided by the assets module, and informs other modules about the activation/deactivation of maturity levels.

As detailed in the YAMM pools section, we strive to automatically add pASSETs to corresponding YAMM pools when they are activated. The listener is implemented for this purpose and, whenever a new maturity is activated, if its lifetime exceeds the introduction and expiration time intervals, we introduce the pASSET to the corresponding YAMM pool.

It's worth noting that if the yamm pool hasn't been created yet, we create a new yamm pool with all relevant active pASSETs. If the pool is created but not initialized yet, we do not use virtual balance for adding the token.

State

Pulse-Trade Order

Pulse-trade orders are stored as:

(
[]byte("v1/order/") |
[]byte(orderId)
) -> ProtoBuf(Order)

Pulse-Trade Order Count

The pulse-trade orders count is utilized to tally the total number of orders created in the system so far and is used to generate identifiers for orders. It is stored as:

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

Executable Order

Orders in the execution queue are stored as:

(
[]byte("v1/executable-order/") |
[]byte(whitelistedRoute) | // 1 if whitelistedRoute, else 0
[]byte(poolId) |
[]byte(tokenIn) |
[]byte("|") |
[]byte(tokenOut) |
[]byte("|") |
[]byte(MaxStepSpotPrice) | // bigInt bytes in a padded array of length 40
[]byte(orderId) |
) -> []byte(orderId)

Executable Order Count

The count of executable orders per pair is stored as:

(
[]byte("v1/executable-order-count/") |
[]byte(whitelistedRoute) | // 1 if whitelistedRoute, else 0
[]byte(poolId) |
[]byte(token1) | // min(tokenOut, tokenIn)
[]byte("|") |
[]byte(token2) | // max(tokenOut, tokenIn)
) -> ProtoBuf(ExecutableOrderCount)

An ExecutableOrderCount is:

message ExecutableOrderCount {
uint64 pool_id = 1;
string token_in = 2; // tokenIn < tokenOut
string token_out = 3; // tokenIn < tokenOut
bool whitelisted_route = 4;
uint64 count = 5;
}

Schedule Order

Orders in the schedule queue are stored as:

(
[]byte("v1/schedule-order/") |
[]byte(timeMillis) |
[]byte(orderId) |
) -> ProtoBuf(ScheduleOrder)

A schedule order is:

message ScheduleOrder {
int64 time_millis = 1;
uint64 order_id = 2;
}

Schedule Time By Order

To find the schedule time for a specific order, we store the following index:

(
[]byte("v1/schedule-time-by-order/") |
[]byte(orderId)
) -> []byte(TimeMillis)

Whitelisted Route

Whitelisted routes for multistep pulse-trade are stored as:

(
[]byte("v1/whitelisted-route/") |
[]byte(token1) | // min(tokenOut, tokenIn)
[]byte("|") |
[]byte(token2) | // max(tokenOut, tokenIn)
) -> ProtoBuf(ExecutableOrderCount)

Here, tokenIn is whitelistedRoute.Steps[0].TokenIn while tokenOut is whitelistedRoute.Steps[*len*(whitelistedRoute.Steps)-1].TokenOut.

Expiring Pool Token

We store the following data for the virtual balance of pool tokens being removed from a pool:

(
[]byte("v1/expiring-pool-token/") |
[]byte(poolId) |
[]byte(denom)
) -> ProtoBuf(VirtualBalancePoolToken)

Introducing Pool Token

For pool tokens entering a pool, we store the following data:

(
[]byte("v1/introducing-pool-token/") |
[]byte(poolId) |
[]byte(denom)
) -> ProtoBuf(VirtualBalancePoolToken)

Oracle Price Pair

We store oracle price pairs as:

(
[]byte("v1/oracle-price-pair/") |
[]byte(assetId)
) -> ProtoBuf(OraclePricePair)

Parameters

Module parameters are stored:

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

Pending Token Introduction

Pending token introductions are stored:

(
[]byte("v1/pending-token-introduction/") |
[]byte(length(assetId)) |
[]byte(assetId) |
[]byte(targetPoolId)
) -> ProtoBuf(Order)

Pool Pending Token Introduction Count

The count of pending token introductions for a specific pool is stored:

(
[]byte("v1/pool-pending-token-introduction-count/")|
[]byte(poolId)
) -> []byte(count)

Pool

Pools are stored:

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

Pool Count

The pool count is used to track the total number of pools created, and aids in generating identifiers for pools:

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

Pool Token

Pool tokens are stored:

(
[]byte("v1/pool-token/") |
[]byte(poolId) |
[]byte(denom) |
) -> ProtoBuf(PoolToken)

Pool LP Token Supply

LP supply of pools is stored:

(
[]byte("v1/pool-lp-token-supply/") |
[]byte(poolId) |
) -> ProtoBuf(supply)

Vault Pause

We store the vault pause status:

(
[]byte("v1/vault-paused/")
) -> []byte(paused) // paused -> 1, o.w. -> 0

Weight Update Timing

Weight update timings of weighted pools are stored:

(
[]byte("v1/weight-update-timing/") |
[]byte(poolId)
) -> ProtoBuf(WeightUpdateTiming)

Weighted Token

We store specific data of tokens in weighted pools as:

(
[]byte("v1/weighted-token/") |
[]byte(poolId) |
[]byte(denom)
) -> ProtoBuf(PoolToken)

Asset ID in YAMM Pool

For locating a specific asset's YAMM pool, the following index is stored:

(
[]byte("v1/asset-by-yamm-pool-prefix/") |
[]byte(poolId)
) -> []byte(assetId)

YAMM Pool's Asset ID

To identify the asset associated with a particular YAMM pool, we store this index:

(
[]byte("v1/yamm-pool-by-asset-prefix/") |
[]byte(assetId)
) -> []byte(poolId)

YAMM Configuration

Each YAMM pool's specific configuration is stored as follows:

(
[]byte("v1/yamm-configuration/") |
[]byte(poolId)
) -> ProtoBuf(YammConfiguration)

Parameters

The module's parameters are detailed throughout the document. They are stored in this structure:

// Params defines the parameters for the module.
message Params {
GeneralPoolParameters general_pool_parameters = 1 [(gogoproto.nullable) = false];
YammParameters yamm_parameters = 2 [(gogoproto.nullable) = false];
OrderParameters order_parameters = 3 [(gogoproto.nullable) = false];
AuthorizationParameters authorization_parameters = 4 [(gogoproto.nullable) = false];
}

message AuthorizationParameters {
repeated string admin_list = 1;
// can pause the vault and also set pools to paused_by_gov mode which
// is a special mode where only the gov can unpause and does not have a window
// these cannot unpause anything
repeated string pause_allow_list = 2;
}

message OrderParameters {
string step_matching_fee_ratio = 1 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string step_swap_fee_ratio = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string matching_protocol_fee_ratio = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string matching_solver_fee_ratio = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
int32 max_orders_per_block = 5;
int32 max_schedule_per_block = 6;
string max_exec_order_trade_ratio = 7 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string max_order_step_ratio = 8 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string min_order_step_ratio = 9 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

message YammParameters {
string lambda = 1 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// duration (milliseconds) for virtual balance when adding new pAssets to yamm pools
int64 maturity_introduction_interval_millis = 2;
int64 maturity_expiration_interval_millis = 3;

string introduction_virtual_balance_scaler = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

string expiration_virtual_balance_scaler = 5 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

string buy_y_given_in_loan_fee_ratio = 6 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

string sell_y_given_out_fee_ratio = 7 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

string max_alpha = 8 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// this will be set to newly created yamm pools
// if not empty, only these addresses can initialize the pools
repeated string default_initialization_allow_list = 9;

string avg_monthly_yield_rate = 10 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

string yield_fee_scaler = 11 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// this will be set to newly created yamm pools
repeated string default_admins = 12;
// this will be set to newly created yamm pools
repeated string default_pause_allow_list = 13;
}

message GeneralPoolParameters {
bool allow_public_pool_creation = 1;
string default_swap_fee_ratio = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string swap_protocol_fee_ratio = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string join_exit_protocol_fee_ratio = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

The Params message outlines various parameters, subdivided into four messages: GeneralPoolParameters, YammParameters, OrderParameters, and AuthorizationParameters. Each division is explained below:

GeneralPoolParameters

  • allow_public_pool_creation: Boolean value determining whether users can create pools. If set to false, only the governance can create pools.
  • default_swap_fee_ratio: This is the default swap fee ratio, applicable when creating a new yamm pool automatically.
  • swap_protocol_fee_ratio: Represents the swap fee portion charged as a protocol fee.
  • join_exit_protocol_fee_ratio: Represents the join/exit input portion charged as a protocol fee.

YammParameters

  • pool_id: Identifier for the yamm pool.
  • lambda: The λ\lambda parameter of the pool.
  • maturity_introduction_interval_millis: Duration, in milliseconds, for the virtual balance when introducing new pAssets to YAMM pools.
  • maturity_expiration_interval_millis: Duration, in milliseconds, for the virtual balance when removing pAssets from YAMM pools.
  • introduction_virtual_balance_scaler: Scaling parameter for the virtual balance of introduced pAssets.
  • expiration_virtual_balance_scaler: Scaling parameter for the virtual balance of expired pAssets.
  • buy_y_given_in_loan_fee_ratio: Fee ratio for buying yASSET given in.
  • sell_y_given_out_fee_ratio: Fee ratio for selling yASSET given out.
  • max_alpha: The maximum value for the α\alpha parameter.
  • default_initialization_allow_list: Used as the initial value for the initialization_allow_list of YAMM pools during creation.
  • avg_monthly_yield_rate: The rr parameter used in trading fee equations.
  • yield_fee_scaler: The ν\nu parameter used in trading fee equations.
  • default_admins: Used as the initial value for the admins of YAMM pools during creation.
  • default_pause_allow_list: Used as the initial value for the pause_allow_list of YAMM pools during creation.

OrderParameters

  • step_matching_fee_ratio: Fee ratio for matching steps in pulse trades.
  • step_swap_fee_ratio: Fee ratio for swapping steps against pools in pulse trades.
  • matching_protocol_fee_ratio: Fee ratio charged upon executing match proposals.
  • matching_solver_fee_ratio: Portion of the matching amounts paid as a reward to the match proposer.
  • max_orders_per_block: Maximum number of orders executable per block.
  • max_schedule_per_block: Maximum number of orders processable from the schedule queue per block.
  • max_exec_order_trade_ratio: Maximum ratio of a token's liquidity in the pool that can be traded against the pool in the order execution procedure.
  • max_order_step_ratio: Maximum ratio of a token's liquidity in the pool that can be set as a step amount in orders.
  • min_order_step_ratio: Minimum ratio of a token's liquidity in the pool that can be set as a step amount in orders.

AuthorizationParameters

  • admin_list: Addresses in this list are allowed to create weighted pools, manage oracle price pairs, and whitelisted routes.
  • pause_allow_list: Addresses in this list are allowed to pause the vault as well as pausing pools instead of governance. It is important to mention that, these addresses are not allowed to unpause anything.

Genesis

The genesis state of this module is structured as follows:

message GenesisPoolData {
Pool pool = 1 [(gogoproto.nullable) = false];
string total_lp_token_supply = 2 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
repeated PoolToken pool_token_list = 3 [(gogoproto.nullable) = false];
}

message YammPoolAssetId {
uint64 pool_id = 1;
string asset_id = 2;
}

// GenesisState defines the amm module's genesis state.
message GenesisState {
Params params = 1 [(gogoproto.nullable) = false];
repeated GenesisPoolData pool_list = 2 [(gogoproto.nullable) = false];
repeated WeightedPoolProperties weighted_pool_properties_list = 3 [(gogoproto.nullable) = false];
repeated YammPoolAssetId yamm_pool_asset_id_list = 4 [(gogoproto.nullable) = false];
repeated VirtualBalancePoolToken introducing_pool_token_list = 5 [(gogoproto.nullable) = false];
repeated VirtualBalancePoolToken expiring_pool_token_list = 6 [(gogoproto.nullable) = false];
repeated YammConfiguration yamm_configuration_list = 7 [(gogoproto.nullable) = false];
repeated WhitelistedRoute whitelisted_route_list = 8 [(gogoproto.nullable) = false];
repeated Order order_list = 9 [(gogoproto.nullable) = false];
uint64 order_count = 10;
repeated uint64 executable_order_list = 11;
repeated ScheduleOrder schedule_order_list = 12 [(gogoproto.nullable) = false];
bool vault_paused = 13;
repeated OraclePricePair oracle_price_pair_list = 14 [(gogoproto.nullable) = false];
repeated PendingTokenIntroduction pending_token_introduction_list = 15 [(gogoproto.nullable) = false];
// this line is used by starport scaffolding # genesis/proto/state
}