Yield AMM
Yield AMM (YAMM) pools cater to the unique requirements of principal and yield token trading. Each refractable asset has a corresponding pool, comprising cAsset and active pAsset tokens as liquidity. As time progresses and new maturity levels are introduced (or old ones expire), the amm module automatically updates the related pAssets in the respective pools.
Preliminaries
The refractor exchange rate is the ratio of the total supply of pAsset to the cAsset amount in the Refractor vault.
This rate at time is denoted as . If a user refracts a cAsset amount , they will receive of pAsset and an identical amount of yAsset.
The yield rate to maturity for a specific pAsset can be calculated using the formula
where is the pAsset's market price (in terms of the cAsset) and is the pAsset's time to maturity.
Design
YAMM utilizes Balancer's constant weighted geometric mean formula, expressed as
where, for each , and denote the balance and weight of token in the pool, respectively, and is a real number. Token represents the cAsset while tokens , , , are the pool's pAssets.
In the YAMM model, weights vary over time. We also incorporate virtual balances and leverage parameters to ensure that the balance curves for the cAsset and any pAsset progressively flatten, mirroring the balance curve of the YieldSpace AMM. YAMM trading adheres to standard Balancer formulas.
To account for a token's time to maturity, we introduce the parameter, computed as:
In this formula, signifies each pAsset, and denote the initial and maturity time of the pAsset, respectively, and represents the current time. Note that equals 0 and equals 1. For all , is set to 1. The expression represents the scaled time to maturity of token , which varies from 0 to 1.
Leverage Parameters
The YAMM utilizes leverage parameters for each pAsset in the pool to enhance pricing precision for traders and replicate the YieldSpace curve. Denoted as , these parameters are dynamic and time-dependent. For each , we define the leverage parameter as:
To avoid numerical issues when nears , it's essential to cap the leverage parameter ; we set this limit at . It's worth noting that in practice, we enforce a configurable maximum parameter to limit the alpha parameter, not the fixed value used here.
Lambda Parameter
The lambda parameter is another leverage parameter for the cAsset, denoted by . This parameter is constant for each pool and must meet the condition . A larger results in flatter curves and better trader prices, but less price discovery. We suggest a default , although this can be configured per pool.
Parameter
Parameter gauges the pool's liquidity. At all times, should equal the number of LP tokens in circulation. We also recommend that the initial number of LP tokens assigned to the pool creator—or the initial liquidity provider—should equal the quantity of cAsset deposited when the pool is established. As such, 's initial value is .
Virtual Adjustment Balances
To facilitate smooth transitions when adding or removing pAssets from the pool, we introduce virtual adjustment balances. These are amounts added to the YAMM pool's virtual balances, and are time-dependent functions. Generally zero, these balances deviate from zero when a new token is added to the pool or an existing one is removed. In these cases, the corresponding virtual adjustment balance will incrementally increase or decrease. Each pAsset in the pool has a corresponding virtual adjustment balance, denoted by .
Virtual adjustment balances are defined using parameter as follows. For each we define:
The function is defined as:
-
When pAsset is added to the pool:
Here, is when the new pAsset is added to the pool, is the time during which the virtual adjustment balance gradually decreases to , and . In the implementation, corresponds to the configurable property
introduction_virtual_balance_scaler
. can be any appropriate time duration (e.g., a week, fortnight, month) that allows a gradual decay of the virtual adjustment balance. Note that at , and at , . -
When pAsset is removed from the pool, we denote the time of maturity of the pAsset by and the length of the asset removal period by (for instance, a week). We then define as follows:
Here, and . As mentioned in the introduction, we don't use a constant in the implementation. Instead, this constant can be configured through the
expiration_virtual_balance_scaler
property. It's important to note that can be as large as possible, meaning the virtual balance continues to increase even after the interval has ended. This process continues until the token balance is zero, at which point it is removed from the pool. -
If the above conditions do not apply, then for all .
To manage the virtual adjustment balances, we store an object of the following type for introducing or removing tokens:
message VirtualBalancePoolToken {
uint64 pool_id = 1;
string denom = 2;
string target_virtual_balance = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
int64 start_unix_millis = 4;
int64 end_unix_millis = 5;
}
The target virtual balance represents the scaler parameter used in calculating . For introducing tokens it corresponds to the parameter, set as introduction_virtual_balance_scaler
, while for expiring tokens it is set as expiration_virtual_balance_scaler
.
Virtual Balances
The pool has virtual balances for trading, defined as follows. Let represent the real balances of all tokens in the pool. The virtual balances are defined by
and
for .
Weights
The weights of different tokens vary over time. The refractor exchange rate at time is denoted by . We define the weights , , as follows:
and
for all . The weights are then scaled proportionally so that .
Trading Fee
The YAMM is designed for trading among yield-bearing tokens, so it's logical that the trading fee isn't a fraction of the token amounts traded, but rather a percentage of the yield rate. The traditional constant fee rate can be problematic in this setting as it can cause a significant difference in the yield rate of the token if the yield is small or if the pAsset is nearing maturity. To define a meaningful trading fee, we'll create a fee that varies with time. Let denote the expected average monthly yield rate of the pAssets. If we have no information about the expected yield rate, we can set to a constant value of 0.01. For each , let denote the period, in months, between the introduction and maturity of pAsset . This means equals measured in months, where and are the maturity's expiration and introduction times, respectively.
We'll also establish a parameter , initially set to 1. Governance can later adjust this to raise or lower pool fees.
With these parameters in place, we define the fee for each pAsset at time as:
The factor is the yield rate of from introduction to maturity. We can round the parameter to the nearest integer to reduce computation.
Importantly, doesn't depend on the pAsset's price—an intentional design choice to avoid manipulation.
For the cASSET, we define the fee as for all . Now, for , the trading fee for trading assets and at time is:
Pool Interactions
Since yamm pools are based on weighted pool designs, standard operations like swapping, depositing liquidity, and withdrawing liquidity all function as outlined in the weighted pools page.
Zero Impact Join
If a user wants to join a pool using only cASSETs, we need to use non-proportional join methods, which involve underlying swaps. But swaps during joining can cause high price impact. To solve this, we've developed a zero-impact join feature that lets users join a YAMM pool with just cASSETs, minimizing price impact.
The zero-impact join includes two steps:
- Refract a portion of the cASSET into pool maturities.
- Use the remaining cASSET and pASSETs to join the pool.
The user receives LP tokens and yASSETs from the initial refraction.
To refract the correct amount of cASSET for each maturity, we need to get the right proportion of each token to join the pool. If the pool contains tokens with balances , where is the cASSET balance and the others are pASSETs, we calculate the required cASSET amount to refract for each token to match the pASSET balance in the pool:
Here, is the refractor's effective exchange rate, accounting for the protocol fee. The effective exchange rate can be calculated as . After determining the amount of cASSET necessary to match the actual balances in the pool, we can calculate the total cASSET required using the following formula:
This allows us to establish the ratio associated with each pASSET:
In these equations, we assume to iterate through the pool's tokens.
Using these ratios, we can calculate the amount of cASSET to refract for each maturity, represented as , where is the user-supplied input cASSET amount.
YAsset Trading
YAMM pools are designed to facilitate trading between cASSET, pASSET, and yASSETs. However, the pool structure includes just cASSET and pASSET, which makes direct yASSET trades impossible via the weighted pool equations. To allow such trades, we employ flash loans and interact with the refractor module. Note that YAMM pools only support trading between yASSET and cASSET (or vice versa), while trades involving yASSET and pASSETs necessitate two-step batch swaps.
Buying yASSETs: The buying process for yASSETs follows these steps:
- Borrow a cASSET amount, , from ammVault.
- Refract the amount.
- Trade all received pAssets for cAssets to repay the loan, and provide the yAsset to the user.
The loan amount needs precise computation to carry out these steps. Assuming that the Refractor module has a fee ratio of and an exchange rate of , let denote the effective exchange rate of the refractor. Thus, the refracted pASSET would be . As outlined in the steps, we must sell this pAsset for cAsset to recover the loan amount.
We now define a few parameters and functions to simplify our calculations.
In the formula above, stand for the pAsset and cAsset indices, respectively. We also define the function as below for added clarity.
We further define the function as:
We now can apply newton method steps as:
This Newton method applies for a maximum of 15 steps or stops early if the following condition is met:
Recall that this method approximates the loan amount. However, for practical purposes, we need to ensure that we can repay the loan. To make allowances for any estimation errors, we propose a loan fee, which is adjusted by the BuyYGivenInLoanFeeRatio
parameter. This fee is deducted from the before applying the Newton method, and later added back to the amount being refracted. Consequently, we only need to replace in the Newton method with . Nevertheless, when we want to perform the refraction, we use the actual .
Buying yASSET given out: This scenario indicates that we know the yASSET amount and we must calculate the required cASSET. Let's assume that the refractor converts an amount of cASSET into another amount using a function . Given that the output amount is and , we have:
With the quantity , we then refract this amount and acquire amounts of yAsset and pAsset respectively. Due to rounding errors if the opration fails but if we take the required output amount for the user and the remaining is taken as fee. The computation of and amounts can be done by trading for the cAsset. The output amount becomes , and .
The challenge lies in figuring out the value of . Given the refractor equations with roundings and an exchange rate of , we formulate as follows:
This set of equations helps us determine the value of , ensuring the trades can be executed efficiently.
The process can also be given as follows:
We can use the right-hand side of equation (11) as an estimate for , which also satisfies equation (5). Therefore, we get:
Note that if rounding errors were not considered, we would have:
Hence, the impact of considering rounding is accounted for the fraction . If the refractor fee is less than (which is reasonable), this value would be less than . Assuming that the exchange rate is at least 1 (a reasonable assumption), the error would, at most, be 4. Given that cASSET has a 6-decimal place precision, this error translates to cASSET, a relatively insignificant value.
Selling yAssets: If a trader intends to sell a yAsset quantity to gain cAsset, the protocol must perform these steps:
- Borrow a suitable quantity of cAsset.
- Trade the cAsset amount to receive a quantity of pAsset.
- Redeem the amounts of pAsset and yAsset in the refractor module to get cAsset.
- Use the acquired cAsset amount to repay the loan. The remaining cAsset goes to the trader.
In this context, the loan amount does not necessitate complex calculations or estimates. Since we know the required pASSET amount from the trade in step 2, we can execute a swap given out. The AMM's core equations take care of computing the loan amount.
Selling yAssets given out: Let's consider a situation where the trader wants to receive a specific cAsset amount , and we must compute the yAsset quantity that the trader needs to sell.
Let's consider as the effective refractor exchange rate (encompassing the fee). Given that refracting a cASSET amount results in a pAsset quantity of , we get:
Similarly, the cAsset amount can be determined as:
Here, refers to the cAsset index and refers to the pAsset index. Let's define as:
Therefore, and
This leads to:
Applying Newton's method to find the value of . Let's define as:
We proceed with setting and, for each , we define:
Consequently,
Given that the amount is computed iteratively using the Newton method, approximation errors are likely. These errors might cause the operation to fail if the merge output is insufficient to pay the loan amount. To prevent this, we compute a fee using , and calculate the loan amount as if was equal to the 'actual ' plus the fee. For the merge and swap processes, the 'actual ' is used, and the fee is disregarded.
Refractor action computation: Note that all these yAsset trade types include a step of interaction with the refractor module. Given that the vault in this module is different from the AMM module vault, actual bank transfers are necessary to carry out these operations. In normal swaps, as previously described, we don't transfer any amount until the operation's end. At that point, we offset the input/output amounts and then execute the transfers. To maintain this convention here, in each yAsset trade, we don't execute the refract/redeem actions. Instead, we use the refractor API to compute the actions. These actions are aggregated together and executed at the end of the entire swap or batch swap operation.
YAMM Pool Configurations
In the YAMM pool design, we've introduced unique parameters not found in other Weighted pool types. Each pool stores these parameters in the following structure:
message YammConfiguration {
uint64 pool_id = 1;
string lambda = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
// duration (milliseconds) for virtual balance when adding new pAssets to yamm pools
string maturity_introduction_interval_millis = 3 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = true
];
string maturity_expiration_interval_millis = 4 [
(cosmos_proto.scalar) = "cosmos.Int",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = true
];
string introduction_virtual_balance_scaler = 5 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
string expiration_virtual_balance_scaler = 6 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
// if the value is not set, will be read from module parameters
string buy_y_given_in_loan_fee_ratio = 7 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
// if the value is not set, will be read from module parameters
string sell_y_given_out_fee_ratio = 8 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
string max_alpha = 9 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
string avg_monthly_yield_rate = 10 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
string yield_fee_scaler = 11 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = true
];
}
pool_id
: YAMM pool ID.lambda
: Pool's parameter.maturity_introduction_interval_millis
: Duration (in milliseconds) of the virtual balance for new pAssets in YAMM pools.maturity_expiration_interval_millis
: Duration (in milliseconds) of the virtual balance for removed pAssets from YAMM pools.introduction_virtual_balance_scaler
: Scaler for the virtual balance of new pAssets.expiration_virtual_balance_scaler
: Scaler for the virtual balance of removed pAssets.buy_y_given_in_loan_fee_ratio
: Loan fee ratio for purchasing yASSET.sell_y_given_out_fee_ratio
: Fee ratio for selling yASSET.max_alpha
: Maximum number for clipping the parameter.avg_monthly_yield_rate
: The parameter in trading fee equations.yield_fee_scaler
: The parameter in trading fee equations.
If a parameter value is unset, it defaults to the module parameters.