Generalized Solidly Stableswap
Stableswaps are pools that offer low slippage for two assets that are intended to be tightly correlated. There is a price ratio they are expected to be at, and the AMM offers low slippage around this price. There is still price impact for each trade, and as the liquidity becomes more lop-sided, the slippage drastically increases.
This package implements the Solidly stableswap curve, namely a CFMM with invariant:
It is generalized to the multi-asset setting as
Pool configuration
One key concept, is that the pool has a native concept of
Scaling factor handling
An important concept that's up to now, not been mentioned is how do we set the expected price ratio.
In the choice of curve section, we see that its the case that when x_reserves ~= y_reserves, that spot price is very close to 1. However, there are a couple issues with just this in practice:
- Precision of pegged coins may differ. Suppose
1 Foo = 10^12 base units, whereas1 WrappedFoo = 10^6 base units, but1 Foois expected to trade near the price of1 Wrapped Foo. - Relatedly, suppose there's a token called
TwoFoowhich should trade around1 TwoFoo = 2 Foo - For staking derivatives, where value accrues within the token, the expected price to concentrate around dynamically changes (very slowly).
To handle these cases, we introduce scaling factors. A scaling factor maps from "raw coin units" to "amm math units", by dividing.
To handle the first case, we would make Foo have a scaling factor of 10^6, and WrappedFoo have a scaling factor of 1.
This mapping is done via raw coin units / scaling factor.
We use a decimal object for amm math units, however we still have to be precise about how we round.
We introduce an enum rounding mode for this, with three modes: RoundUp, RoundDown, RoundBankers.
The reserve units we pass into all AMM equations would then be computed based off the following reserves:
scaled_Foo_reserves = decimal_round(pool.Foo_liquidity / 10^6, RoundingMode)
descaled_Foo_reserves = scaled_Foo_reserves * 10^6
Similarly all token inputs would be scaled as such. The AMM equations need to each ensure that rounding happens correctly, for cases where the scaling factor doesn't perfectly divide into the liquidity. We detail rounding modes and scaling details as pseudocode in the relevant sections of the spec. (And rounding modes for 'descaling' from AMM eq output to real liquidity amounts, via multiplying by the respective scaling factor)
Algorithm details
The AMM pool interfaces requires implementing the following stateful methods:
SwapOutAmtGivenIn(tokenIn sdk.Coins, tokenOutDenom string, swapFee sdk.Dec) (tokenOut sdk.Coin, err error)
SwapInAmtGivenOut(tokenOut sdk.Coins, tokenInDenom string, swapFee sdk.Dec) (tokenIn sdk.Coin, err error)
SpotPrice(baseAssetDenom string, quoteAssetDenom string) (sdk.Dec, error)
JoinPool(tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, err error)
JoinPoolNoSwap(tokensIn sdk.Coins, swapFee sdk.Dec) (numShares sdk.Int, err error)
ExitPool(numShares sdk.Int, exitFee sdk.Dec) (exitedCoins sdk.Coins, err error)
The "constant" part of CFMM's imply that we can reason about all their necessary algorithms from just the CFMM equation. There are still multiple ways to solve each method. We detail below the ways in which we do so. This is organized by first discussing variable substitutions we do, to be in a more amenable form, and then the details of how we implement each method.
CFMM function
Most operations we do only need to reason about two of the assets in a pool, and sometimes only one. We wish to have a simpler CFMM function to work within these cases. Due to the CFMM equation being a symmetric function, we can without loss of generality reorder the arguments to the function. Thus we put the assets of relevance at the beginning of the function. So if two assets , we write: .
We then take a more convenient expression to work with, via variable substitution.
As a corollary, notice that , which will be useful when we have to compare before and after quantities. We will use as short-hand for this.
Swaps
The question we need to answer for a swap is "suppose I want to swap units of , how many units of would I get out".
Since we only deal with two assets at a time, we can then work with our prior definition of . Let the input asset's reserves be , the output asset's reserves be , and we compute and given the other asset reserves, whose reserves are untouched throughout the swap.
First we note the direct way of solving this, its limitation, and then an iterative approximation approach that we implement.
Direct swap solution
The method to compute this under 0 swap fee is implied by the CFMM equation itself, since the constant refers to: