Skip to main content
Version: v0.19

Weighted AMM

In previous sections, we introduced the fundamental concepts of pools and the vault for pool interaction. This section delves into a unique type of pool, known as a weighted pool, housed within the amm module.

Weighted Math

Weighted pools utilize a constant product equation:

ΠiBiwi=C,\Pi_i B_i^{w_i} = C,

Where BiB_i and wiw_i denote the balance and weight of the iith token in the pool, respectively.

Our implementations incorporate a notion termed virtual balances. This involves augmenting a token's actual balance with a virtual number for specific use-cases. A typical instance is when a new token is introduced into a pool, we generate a virtual balance for the token to regulate its price. The specifics of each use-case will be detailed in the relevant documentation. For now, we'll include virtual balances in the equation as follows:

ΠiViwi=C,\Pi_i V_i^{w_i} = C,

Here, ViV_i represents the virtualized balance of token ii, encompassing both the token's actual balance and virtual amounts.

Pool Interactions

This section outlines the equations used in pool interactions, such as trades, spot prices, liquidity deposit, and withdrawal.

Swaps

Let ϕ\phi denote the trading fee. Our pool trading formulas, similar to Balancer's, account for virtual balances. Specifically, if the real balances in the pool prior to a trade are B1,,BnB_1,\ldots,B_n, the virtual balances are V1,,VnV_1,\ldots,V_n, and the weights are w1,,wnw_1,\ldots,w_n, the quantity AoA_o of token oo received by the trader when depositing an amount AiA_i of token ii is

Ao=Vo(1(ViVi+(1ϕ)Ai)wiwo).A_o = V_o\left(1-\left(\frac{V_i}{V_i+(1-\phi)A_i}\right)^{\frac{w_i}{w_o}}\right).

The amount AiA_i of token ii required to deposit to obtain an amount AoA_o of token oo is

Ai=Vi1ϕ((VoVoAo)wowi1).A_i = \frac{V_i}{1-\phi}\left(\left(\frac{V_o}{V_o-A_o}\right)^{\frac{w_o}{w_i}}-1\right).

If a trader deposits an amount AiA_i of token ii and receives an amount AoA_o of token oo, and B1,B2,,BnB'_1,B'_2,\ldots,B'_n denote the real balances in the pool post-trade, then

Bi=Bi+AiBo=BoAoj{1,2,....,n}{i,o}Bj=Bj\begin{alignedat}{1} B'_i&=B_i+A_i \\ B'_o&=B_o-A_o \\ \forall_{j\in\{1, 2, ...., n\}-\{i,o\}} B'_j&=B_j \end{alignedat}

as expected.

Note that we must ensure Bo>0B'_o>0 to execute the trade, unless the token is being removed from the pool. In this case, we also accept Bo=0B'_o=0 and subsequently remove the token from the pool.

Spot Prices

As alluded to earlier, virtual balances are used in lieu of real balances when trading pool assets. Therefore, for all i,j{1,2,...,n}i,j\in\{1, 2, ..., n\}, the spot price of tokenj\text{token}_j in terms of tokeni\text{token}_i is

 Viwi Vjwj.\frac{\ \frac{V_i}{w_i}\ }{\frac{V_j}{w_j}}.

LP token spot price: To calculate the LP token's price of the pool in terms of tokenitoken_i, we first determine the pool's total value in this token as follows:

Bi+ji Viwi VjwjBj =Viwi(wiViBi+jiwjVjBj)=ViwijwjVjBj,B_i + \sum_{j\neq i} \frac{\ \frac{V_i}{w_i}\ }{\frac{V_j}{w_j}} B_j \ \\ = \frac{V_i}{w_i}\left(\frac{w_i}{V_i}B_i + \sum_{j\neq i} \frac{w_j}{V_j} B_j \right) \\ = \frac{V_i}{w_i} \sum_{j} \frac{w_j}{V_j} B_j, \\

Considering the quantity of LP tokens in circulation as LL, the LP token's spot price in terms of the token becomes

ViLwijwjVjBj .\frac{V_i}{Lw_i} \sum_{j} \frac{w_j}{V_j} B_j\ .

Liquidity Deposit

Proportional all-asset deposits: These deposits follow the standard procedure, where a user deposits amounts of each asset in the pool, proportional to the pool's current balances. Specifically, if B1,,BnB_1,\ldots,B_n are the balances in the pool, a user must deposit amounts A1,,AnA_1,\ldots,A_n of each asset that satisfy

A1B1==AnBn=q.\frac{A_1}{B_1} = \ldots = \frac{A_n}{B_n} = q .

Thus, the user will receive qLqL LP tokens, where LL is the number of LP tokens in circulation. If we consider LL as a scalar in the virtual balance designs, all virtual balances scale by qq. It's straightforward to see that the spot prices remain unaltered after this operation.

Single-asset deposits given LP amount: Here, the user aims to deposit a single asset (token ii) and receive a certain LP token amount. We need to determine the token ii amount the user must deposit.

Suppose cc is the LP token amount the user wishes to receive. Let B1,,BnB_1,\ldots,B_n be the real balances, and V1,,VnV_1,\ldots,V_n are the virtual balances of the pool. Let ϕ\phi be the pool fee. To facilitate the single-asset deposit of token ii, we contemplate several trades of token ii against other tokens, followed by a proportional all-asset deposit.

Let q=cLq=\frac{c}{L}. Note, qq is the pool share corresponding to the cc LP tokens the user wishes to obtain. For j{1,...,n}{i}j\in \{1,...,n\}-\{i\}, let AjA_j be the amount the user acquires from the trades and will be used in the proportional all-asset deposit. Since, for each j{1,...,n}{i}j\in \{1,...,n\}-\{i\}, the pool's real balance of token jj after trades is BjAjB_j-A_j, and the user wishes to receive cc LP tokens which correspond to a qq pool fraction, we deduce that

Aj=q(BjAj) .A_j = q(B_j-A_j) \ .

Hence,

Aj=qBj1+qA_j = \frac{qB_j}{1+q}

for each j{1,...,n}{i}j\in \{1,...,n\}-\{i\}. Let AA denote the quantity of token ii a user must trade in the pool to acquire quantities AjA_j of token jj, for each j{1,...,n}{i}j \in \{1,...,n\}-\{i\}, excluding transaction fees. This relationship is represented by the following equation:

(Vi+A)wij=1jin(VjAj)wj=j=1nVjwj .(V_i+A)^{w_i} \prod_{\substack{j=1 \\ j\neq i}}^{n} (V_j-A_j)^{w_j} = \prod_{j=1}^{n} V_j^{w_j} \ .

From this, we can derive:

A=(j=1nVjwjj=1jin(VjAj)wj)1wiVi .A = \left(\frac{\prod\limits_{j=1}^{n} V_j^{w_j}}{\prod\limits_{\substack{j=1 \\ j\neq i}}^{n} (V_j-A_j)^{w_j}}\right)^{\frac{1}{w_i}} - V_i \ .

When fees are accounted for, the user must trade an amount AA' of token ii, where A=11ϕAA' = \frac{1}{1-\phi}\cdot A. The post-trade balance of token ii becomes Bi+AB_i + A'. If a proportional all-asset deposit equal to a pool share qq is performed after the trades, the user must provide q(Bi+A)q(B_i + A') of token ii. Consequently, the total deposit required from the user is A+q(Bi+A)A' + q(B_i + A') to obtain cc LP tokens through a single-asset deposit of token ii.

Non-proportional multi-asset deposits: In this scenario, the user intends to deposit varying amounts of one or more pool assets, and we must calculate the amount of LP tokens to be issued.

Suppose the real balances of the pool are B1,,BnB_1,\ldots,B_n and the virtual balances are V1,,VnV_1,\ldots,V_n. The user's deposit amounts are A1,,AnA_1,\ldots,A_n. We will assume a feeless pool for this discussion.

To undertake a proportional all-asset deposit, the user would need to exchange appropriate amounts of some assets to acquire more of others so that the user's post-trade asset amounts are proportional to the pool's real balances. Subsequently, the user can make a proportional all-asset deposit with their assets. Let qq represent the user's LP token share for the deposit; hence, the user will receive qLqL LP tokens.

We will determine the value of qq, allowing us to perform a single transaction instead of several trades followed by a proportional all-asset deposit. Note that the pool's real balances post-trade and following the all-asset deposit will be B1+A1,,Bn+AnB_1+A_1,\ldots,B_n+A_n, as the user takes some assets from the pool only to deposit them back shortly after. Consider V1,,VnV'_1,\ldots,V'_n as the virtual balances of the pool post-deposit. Given that the outcome of Balancer's weighted geometric mean formula remains constant with trades and increases in proportion to all-asset deposits, we get:

j=1n(Vj)wj=(1+q)j=1nVjwj .\prod_{j=1}^{n} (V'_j)^{w_j} = (1+q)\prod_{j=1}^{n} V_j^{w_j} \ .

Let's denote the value of the parameter LL after the deposit as LL'. For all j{1,...,n}{i}j\in \{1,...,n\}-\{i\}, there is a zjz_j such that:

VjBj=zjL=zjL(1+q)=(1+q)(VjBj) .V'_j-B'_j = z_j \cdot L' = z_j \cdot L \cdot (1+q) = (1+q)(V_j-B_j) \ .

This gives us:

Vj=Bj+(1+q)(VjBj)=Bj+Aj+(1+q)(VjBj) .V'_j = B'_j + (1+q)(V_j-B_j) = B_j + A_j + (1+q)(V_j-B_j) \ .

Our goal is to find qq that satisfies:

j=1n(Bj+Aj+(1+q)(VjBj))wj=(1+q)j=1nVjwj ,\prod_{j=1}^{n} (B_j + A_j + (1+q)(V_j-B_j))^{w_j} = (1+q)\prod_{j=1}^{n} V_j^{w_j} \ ,

or equivalently:

j=1n(Bj+Aj+(1+q)(VjBj))wj(1+q)j=1nVjwj=0 ,\prod_{j=1}^{n} (B_j + A_j + (1+q)(V_j-B_j))^{w_j} - (1+q)\prod_{j=1}^{n} V_j^{w_j} = 0\ ,

To find the value of qq, we apply Newton's method. Define g(x)g(x) as:

g(x)=j=1n(Bj+Aj+(1+x)(VjBj))wj(1+x)j=1nVjwj .g(x) = \prod_{j=1}^{n} (B_j + A_j + (1+x)(V_j-B_j))^{w_j} - (1+x)\prod_{j=1}^{n} V_j^{w_j} \ .

For each j{1,2,....,n}j\in\{1, 2, ...., n\}, let

hj(x)=Bj+Aj+(1+x)(VjBj).h_j(x) = B_j + A_j + (1+x)(V_j-B_j).

Hence,

g(x)=j=1n(hj(x))wj(1+x)j=1nVjwj .g(x) = \prod_{j=1}^{n} (h_j(x))^{w_j} - (1+x)\prod_{j=1}^{n} V_j^{w_j} \ .

Observe:

g(x)=l=1n(wl(hl(x))wl1(VlBl)j=1jln(hj(x))wj)j=1nVjwj=l=1n(wl(VlBl)hl(x)j=1n(hj(x))wj)j=1nVjwj=j=1n(hj(x))wjl=1n(wl(VlBl)hl(x))j=1nVjwj .\begin{alignedat}{1} g'(x) & = \sum_{l=1}^{n} \left( w_l (h_l(x))^{w_l-1}(V_l-B_l)\prod_{\substack{j=1 \\ j\neq l}}^{n} (h_j(x))^{w_j} \right) - \prod_{j=1}^{n} V_j^{w_j} \\ & = \sum_{l=1}^{n} \left( \frac{w_l (V_l-B_l)}{h_l(x)}\prod_{j=1}^{n} (h_j(x))^{w_j} \right) - \prod_{j=1}^{n} V_j^{w_j} \\ & = \prod_{j=1}^{n} (h_j(x))^{w_j}\sum_{l=1}^{n} \left( \frac{w_l (V_l-B_l)}{h_l(x)} \right) - \prod_{j=1}^{n} V_j^{w_j} \ . \end{alignedat}

We apply Newton's method with:

x0=j=1n(Bi+AiBi)w1x_0 = \prod_{j=1}^{n}\left(\frac{B_i+A_i}{B_i}\right)^w - 1

and define:

xj+1=xjg(xj)g(xj) .x_{j+1} = x_j - \frac{g(x_j)}{g'(x_j)} \ .

Simulations indicate that 10 iterations of Newton's method suffice in most cases. We've established that this operation entails a swap step, which necessitates a swap fee. To streamline swap fee calculations, we apply the fee to the LP amount using the pool's swap fee ratio. As these estimations hinge on the ratio of assets used in swaps versus those used in proportional join, we first execute the largest possible proportional join. The remaining amounts are then calculated using the equations, and the swap fee is only applied to this portion.

Liquidity Withdrawals

Proportional all-asset withdrawals: These are conducted in the usual manner, where users redeeming a certain quantity of LP tokens receive proportional amounts of all pool assets, corresponding to their pool share. Specifically, if a user redeems an amount cc of LP tokens and the pool's real balances are B1,,BnB_1,\ldots,B_n, the user will receive, for each j{1,...,n}j\in \{1, ..., n\}, an amount equal to cLBj\frac{c}{L}B_j of asset jj of the pool. Here, LL represents the number of LP tokens in circulation.

As with the all-asset deposit case, we'll now demonstrate that spot prices remain unchanged after an all-asset liquidity withdrawal. Let's assume B1,,BnB'_1,\ldots,B'_n are the pool's real balances post-withdrawal, V1,,VnV'_1,\ldots,V'_n are the virtual balances post-withdrawal, and LL' represents the value of parameter LL post-withdrawal. Let q=cLq=\frac{c}{L}. Note that for all j{1,...,n}j\in \{1, ..., n\},

Bj=BjAj=BjqBj=(1q)BjB'_j = B_j - A_j = B_j - qB_j = (1-q)B_j

and

L=LqL=(1q)L .L' = L - qL = (1-q)L \ .

If we use parameter LL as a scaler in the virtual balances, they will change proportionally to the real balances and parameter LL, implying that spot prices remain unchanged after an all-asset liquidity withdrawal.

Single-asset withdrawals given LP amount: Here, the user redeems a specific quantity of LP tokens and wishes to receive only token ii, instead of a proportional amount of all pool assets. We aim to calculate the quantity of token ii the user will receive.

Let cc represent the quantity of LP tokens the user wishes to redeem. Let B1,,BnB_1,\ldots,B_n be the real balances and V1,,VnV_1,\ldots,V_n the virtual balances of the pool. Let ϕ\phi represent the pool fee. To provide the liquidity provider with only token ii, we'll consider a proportional all-asset withdrawal followed by several trades. For simplicity, we'll apply the trading fee to token ii. Define qq as the pool's share corresponding to cc LP tokens to be redeemed, i.e., q=cLq=\frac{c}{L}. Let B1,,BnB'_1,\ldots,B'_n and V1,,VnV'_1,\ldots,V'_n denote the real and virtual balances of the pool post-liquidity withdrawal, respectively. As per the proportional all-asset withdrawal case, for all j{1,...,n}j \in \{1,...,n\}, we have Vj=(1q)VjV'_j = (1-q)V_j. Therefore,

j=1n(Vj)wj=(1q)j=1nVjwj .\prod_{j=1}^{n} (V'_j)^{w_j} = (1-q)\prod_{j=1}^{n} V_j^{w_j} \ .

Let AA represent the total amount of token ii the user will obtain after the corresponding trades, if there were no fees. Let B1,,BnB''_1,\ldots,B''_n and V1,,VnV''_1,\ldots,V''_n denote the real and virtual balances of the pool after the trades. We have

j=1n(Vj)wj=j=1n(Vj)wj ,\prod_{j=1}^{n} (V''_j)^{w_j} = \prod_{j=1}^{n} (V'_j)^{w_j} \ ,

and

Bj={Bjfor jiBiAfor j=i .B''_j = \left\{ \begin{array}{cl} B_j & \textnormal{for }j\neq i \\ B_i - A & \textnormal{for } j = i \ .\end{array} \right.

We observe that

VjBj=VjBj=(1q)(VjBj) for all j{0,1,...,n}.V''_j - B''_j = V'_j - B'_j = (1-q)(V_j - B_j) \ \textnormal{for all } j \in \{0,1,...,n\}.

Hence,

(1q)j=1nVjwj=j=1n(Vj)wj=j=1n(Vj)wj=j=1n(Bj+VjBj)wj=j=1n(Bj+(1q)(VjBj))wj=(BiA+(1q)(ViBi))wij=1jin(Bj+(1q)(VjBj))wj .\begin{alignedat}{1} (1-q)\prod_{j=1}^{n} V_j^{w_j} &= \prod_{j=1}^{n} (V'_j)^{w_j} = \prod_{j=1}^{n} (V''_j)^{w_j} = \prod_{j=1}^{n} (B''_j + V''_j - B''_j )^{w_j} \\ & = \prod_{j=1}^{n} (B''_j + (1-q)(V_j - B_j) )^{w_j} \\ & = (B_i-A+(1-q)(V_i-B_i))^{w_i}\prod_{\substack{j=1 \\ j\neq i}}^{n} (B_j + (1-q)(V_j - B_j) )^{w_j} \ . \end{alignedat}

Therefore,

A=Bi+(1q)(ViBi)((1q)j=1nVjwjj=1jin(Bj+(1q)(VjBj))wj)1wi .A = B_i + (1-q)(V_i-B_i) - \left(\frac{(1-q)\prod\limits_{j=1}^{n} V_j^{w_j}}{\prod\limits_{\substack{j=1 \\ j\neq i}}^{n} (B_j + (1-q)(V_j - B_j) )^{w_j}} \right) ^{\frac{1}{w_i}} \ .

This formula provides the amount a liquidity provider should receive without fees. However, as we charge a fee on token ii, the user receives

A=qBi+(1ϕ)(AqBi) .A' = qB_i + (1-\phi) (A - q B_i) \ .

Here, the trading fee is charged on the amount AqBiA - q B_i because the user first receives an amount qBiqB_i of token ii with the proportional withdrawal, so this amount should be fee-exempt.

Non-proportional multi-asset withdrawal: This operation can be executed using the deposit equations with negative amounts for the given amounts.

Pool Initialization

To initialize a pool, we receive the balance for each token and mint the appropriate number of liquidity provider (LP) tokens. The number of LP tokens required for initialization is computed as follows:

LP=invariant×len(tokens)invariant=ΠbiwiLP = invariant \times len(tokens) \\ invariant = \Pi b_i^{w_i}

In the above formula, tokenstokens represents the list of all tokens in the pool, and bi,wib_i, w_i denote the initial balance and normalized weight of the ii'th token, respectively.

Token Weights

As outlined in the weighted pool equations, each token in the pool should have an assigned weight. Consequently, we store the following data for each token of a weighted pool:

message WeightedToken {
uint64 pool_id = 1;
string denom = 2;
string normalized_start_weight = 3 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
string normalized_end_weight = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
}

The pool_id and denom serve as keys to reference a specific token in a particular pool, while the remaining parameters are used to determine the token weights. These weights can change linearly over time, shifting from a normalized_start_weight to a normalized_end_weight within a specified time range per pool. This time range is stored in the following structure:

message WeightUpdateTiming {
uint64 pool_id = 1;
int64 start_unix_millis = 2;
int64 end_unix_millis = 3;
}

A pool's owner can request a weight update process based on the timing and target token weights. Weights are then programmed to transition linearly within the given time range from their current value to the target value.

With this model, token weights are always computed on demand using the following methods:

func (m *WeightUpdateTiming) CalculateProgress(blockTime time.Time) sdk.Dec {
currentTime := blockTime.UTC().UnixMilli()
// if ended -> progress is one
if m.EndUnixMillis <= currentTime {
return sdk.OneDec()
}
// if not started yet -> progress is zero
if currentTime <= m.StartUnixMillis {
return sdk.ZeroDec()
}

rangeLength := sdk.NewDec(m.EndUnixMillis - m.StartUnixMillis)
elapsed := sdk.NewDec(currentTime - m.StartUnixMillis)

// We don't need to consider zero division here as this is covered above.
return QuoDown(elapsed, rangeLength)
}

func (m *WeightedToken) CalculateNormalizedWeight(changeProgress sdk.Dec) sdk.Dec {
start := m.NormalizedStartWeight
end := m.NormalizedEndWeight
if changeProgress.GTE(sdk.OneDec()) || start.Equal(end) {
return end
}

if changeProgress.IsZero() {
return start
}

if start.GT(end) {
delta := MulDown(changeProgress, start.Sub(end))
return start.Sub(delta)
} else {
delta := MulDown(changeProgress, end.Sub(start))
return start.Add(delta)
}
}

Introducing Tokens

You cannot add a new token with a zero balance to an existing and initialized weighted pool (a type of balancer pool). To include a new token UU, we'll use a virtual adjustment balance, which will gradually decrease for the new token. This will stimulate arbitrage opportunities, increasing the real balance of the new token.

Let w>0w>0 be the desired pool weight of the new token. Let wSw_S and BSB_S denote the weight and balance of token SS in the pool, respectively. Let q0q_0 be the lower price bound for the new token UU in terms of token SS. Let L(t)\mathcal{L}(t) be the LP supply of the pool at time tt. We define the initial balance AA at introduction time t0t_0 as:

A=2BSwwSq0(1w)L(t0).A=\frac{2 \cdot B_S \cdot w}{w_S \cdot q_0 \cdot(1-w)\cdot \mathcal{L}(t_0)} .

AA is only computed at introduction time and remains unchanged thereafter.

The virtual adjustment balance of the new token is given by

V(t)=L(t)(AATmin{tt0,T}), for tt0,V(t)=\mathcal{L}(t)\left(A-\frac{A}{T} \cdot \min \left\{t-t_0, T\right\}\right), \quad \text { for } t \geq t_0,

where t0t_0 is when the new token is added to the pool, and TT is the time over which the virtual adjustment balance decreases to 00. The time TT can be any suitable duration (a week, fifteen days, a month, etc.) that allows a slow decay of the virtual adjustment balance. Note that V(t0)=A×L(t)V\left(t_0\right)=A\times \mathcal{L}(t) and V(t)=0V(t)=0 for all tt0+Tt \geq t_0+T. Upon introducing a new token UU, we assign it a weight ww and adjust the weights wjw_j of the existing tokens in the Balancer-type pool as follows:

wj(1w)wj.w_j \mapsto(1-w) \cdot w_j .

This ensures that the sum of the weights for all tokens in the pool remains equal to 1. Let's denote the weight of token SS in the pool after adding token UU as wSw_S^{\prime}. Immediately after the introduction of token UU, the spot price of token UU in terms of token SS is

pU=BSwSBUwU=BSwUBUwS=BSwL(t)A(1w)wS=wSq0(1w)BSw2BSw(1w)wS=q02,p_U=\frac{\frac{B_S}{w_S^{\prime}}}{\frac{B_U}{w_U}}=\frac{B_S \cdot w_U}{B_U \cdot w_S^{\prime}}=\frac{B_S \cdot w}{\mathcal{L}(t)\cdot A \cdot(1-w) \cdot w_S}=\frac{w_S \cdot q_0 \cdot(1-w) \cdot B_S \cdot w}{2 \cdot B_S \cdot w \cdot(1-w) \cdot w_S}=\frac{q_0}{2},

This price is lower than the actual price of token UU in terms of token SS. As the virtual balance of token UU drops to 00, the spot price of token UU will gradually increase. At some point, arbitrageurs will find it profitable to sell token UU to the pool, thus enhancing its real balance.

Please note that the sum of the real balance and the virtual balance is used as the token's balance in the pool when executing trades.

For newly introduced tokens, the following record structure is created:

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;
}

Adding YAMM LPT

As explained in the Yield AMM section, our unique pool comprises cASSETs and pASSETs associated with a specific asset. In PRYZM, we endeavor to facilitate the creation of weighted pools that include the LP token from these pools, offering users an easy way to navigate multiple Yield AMMs. While creating such a pool is simple, introducing a new asset to the system and adding its LP to an existing pool requires a token introduction process. This process is explained in detail below.

We've already covered token introduction for weighted pools. However, to introduce new tokens to a weighted pool, we need its price in terms of the tokens already present in the pool. The amm module leverages PRYZM Oracle and price feeders to fulfill this requirement.

The steps to introduce new LP tokens to a nested pool are as follows:

  1. Governance creates an oracle price pair. This structure informs the feeders on how to compute the price of the underlying asset in terms of a specific quote token in the target weighted pool.
message Pair {
string base = 1;
string quote = 2;
string pool_id = 3; //refers to the data source pool (e.g. osmosis gamm pool)
string data_source = 4;
}

// TwapAlgorithm enumerates the valid algorithms for twap_algorithm.
enum TwapAlgorithm {
option (gogoproto.goproto_enum_prefix) = false;
TWAP_ALGORITHM_ARITHMETIC = 0 [(gogoproto.enumvalue_customname) = "ArithmeticTwapAlgorithm"];
TWAP_ALGORITHM_GEOMETRIC = 1 [(gogoproto.enumvalue_customname) = "GeometricTwapAlgorithm"];
}

message OraclePricePair {
string asset_id = 1;
// this is the token denom which should exist in the target weighted pool in pryzm chain
// the reason for adding this property and not using the pairs, is that the token denom in various chains might be different
// for example usdc token might have contract or ibc denom on different chains with different channel and ids
string quote_token = 2;
uint64 twap_duration_millis = 3;
TwapAlgorithm twap_algorithm = 4;
bool disabled = 5;
repeated Pair pairs = 6 [
(gogoproto.castrepeated) = "Pairs",
(gogoproto.nullable) = false
];
// this is the denom of the base token on this chain
// should be ibc denom for most cases
string base_denom = 7;
}

The OraclePricePair message comprises five fields and one repeated field:

  • asset_id: A string denoting the asset identifier. For instance, to introduce the LP token of Luna YAMM pool, assetId of the Luna asset would be used.
  • quote_token: A string denoting the quote token identifier, which should already exist in the target weighted pool.
  • twap_duration_millis: An integer representing the TWAP calculation duration in milliseconds.
  • twap_algorithm: An enum indicating the TWAP calculation algorithm used by the price feeders.
  • disabled: A boolean indicating whether the oracle price pair is disabled. Disabled price pairs do not get price data from feeders.
  • pairs: A route of pairs, including the pairs of base and quote tokens used to calculate the asset price in terms of the quote token. Each Pair message has three fields: base, quote, and pool_id. pool_id is a string denoting the data source pool identifier (e.g., an Osmosis Gamm pool), and base and quote are strings denoting the base and quote tokens of the pair.

Upon having a price-pair, pool creator (or someone from administration list of the pool) can send a message to introduce the LPT to the target weighted pool. The message should include:

  1. Target weighted pool ID.
  2. Yamm pool ID.
  3. New token weight in the pool. Post-introduction, the LP token will have this weight in the weighted pool.
  4. Virtual balance interval, which is the time window discussed in the token introduction section. It starts from a computed value and gradually decreases to zero during this window.

We then first check the pool. If it's not initialized, we simply add the token to the pool with zero balance. Otherwise, we initiate the introduction by creating a pending token introduction with the following structure:

message PendingTokenIntroduction {
string asset_id = 1;
uint64 target_pool_id = 2;
string token_denom = 3;
string token_normalized_weight = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
int64 virtual_balance_interval_millis = 5;
}

In addition to establishing the pending token introduction, we also need governance to activate the oracle price pair, enabling price feeders to report the asset's price in terms of the specified quote.

  1. The oracle feeders report the price in the following payload:
message OraclePayloadDataSourceBlockHeight {
string data_source = 1;
ibc.core.client.v1.Height block_height = 2 [(gogoproto.nullable) = false];
}

// OraclePayload defines the structure of oracle vote payload
message OraclePayload {
repeated OraclePayloadDataSourceBlockHeight data_source_block_heights = 1 [(gogoproto.nullable) = false];
string price = 2 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
repeated Pair pairs = 3 [
(gogoproto.castrepeated) = "Pairs",
(gogoproto.nullable) = false
];
string quote_token = 4;
}

We calculate the LP token's price lower bound in terms of the specified quote. This is done by first determining a lower bound on the LP price in terms of the asset, then multiplying it by the given asset price.

Suppose we have the asset price in terms of a token in the weighted pool, denoted SS. To obtain a lower limit for the LP token price (qq) minted by a YAMM pool in terms of the chosen token SS, we proceed as follows. Let B0B_0 be the cAsset balance in the YAMM pool, cc be the cAsset price in terms of the corresponding Asset, and pp be the Asset price (or a suitable lower limit for it) in terms of token SS. Let LL be the existing LP tokens amount of the YAMM pool, and BB be the YAMM pool's total value in terms of SS. Then

q=BLB0cpL,q=\frac{B}{L} \geq \frac{B_0 \cdot c \cdot p}{L},

and we take

q0=B0cpLq_0=\frac{B_0 \cdot c \cdot p}{L}

as the lower limit for the equations in the token introduction section. Following the equations in that section, we can add the new token with a virtual balance.

Adding Base Tokens

We not only plan to have a weighted pool of YAMM LP tokens, but also of the pools' actual underlying assets. This pool facilitates instant batch swaps from the base assets to any other asset. Like the nested pool discussed earlier, we want the capability to add new refractable assets over time, necessitating the base tokens' addition to this weighted pool.

The process of introducing base tokens to a weighted pool is quite similar to that for the LP tokens, except for the price computation segment. As we have oracle feeders reporting the base asset's price, we don't need to contemplate the equations to compute the LP token's price. We can simply use the reported price for token introduction and virtual balance calculations.

Token Removal

Just as we may need to introduce new tokens into the pool, we may also need to remove tokens. To extract a specific token UU from the pool, we will again utilize virtual balances. The token removal process involves two steps. First Step: Let's select an appropriate time period TT (such as one week) and gradually escalate the virtual adjustment balance for token UU. Specifically, if t1t_1 is the initiation time of this step, and BUB_U represents the balance of token UU at time t1t_1, we establish the virtual adjustment balance as follows:

V(t)=BUL(t1)(tt1)TL(t), for tt1V(t)=\frac{B_U}{\mathcal{L}(t_1)} \cdot \frac{\left(t-t_1\right)}{T}\cdot \mathcal{L}(t), \quad \text { for } t \geq t_1

Consequently, V(t1+T)=BUV\left(t_1+T\right)=B_U, meaning that after the time period TT, the virtual adjustment balance is equivalent to the balance of token UU at the commencement of the first step.

Undoubtedly, this strategy will cause the price of token UU to decrease gradually, incentivizing arbitrageurs to purchase token UU from the pool. It's crucial that we prohibit traders from selling token UU to the pool during this phase, as it could hinder the token's removal.

Second Step: As soon as the actual balance of token UU in the pool reduces to 00, we activate a special function to remove this token from the pool. Note that the weights of the remaining pool tokens must be proportionally adjusted to maintain a sum of 11. Specifically, if wUw_U is the weight of token UU, we adjust the weights wjw_j of the other pool tokens in the following manner when the token removal function is invoked:

wjwj1w.w_j \mapsto \frac{w_j}{1-w} .

This ensures that the sum of the weights of the remaining pool tokens remains 11.

Similar to the process of introducing tokens, we form an expiring virtual token for these tokens with the following structure:

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;
}