get_D() and get_y() in Curve StableSwap
This article shows algebraically step-by-step how the code for get_D()
and get_y()
are derived from the StableSwap invariant.
Given the StableSwap Invariant:
$$
An^n\sum x_i +D=An^nD+\frac{D^{n+1}}{n^n\prod x_i}
$$
There are two frequent math operations we wish to conduct with it:
- Compute $D$ given fixed values for $A$, and the reserves $x_1,\dots,x_n$. Note that $n$, the number of coins the pool supports, is fixed at the time the pool is deployed. This is what the function
get_D()
does. - Given $D$, we wish to increase the value of one of the reserves $x_i$ to a new value $x_i’$ and figure out how much another reserve $x_j$ needs to decrease to keep the equation balanced. This is what the function
get_y()
does. Here “y” means $x_i’$.
These operations are called get_D()
and get_y()
, respectively, in Curve StableSwap.
The objective of get_D()
In Curve V1 (StableSwap), D
behaves similarly to k
in Uniswap V2 — the larger D
is, the more the reserves are, and the “further out” the price curve will be. D
changes — and needs to be recomputed — after liquidity is added or removed, or a fee changes the pool balance. This is what the function get_D()
is for. Given the current reserves of the pool, it computes D
.
If a curve pool holds two tokens, x
and y
, the StableSwap invariant is
$$
4A(x + y) + D = 4AD+\frac{D^3}{4xy}
$$
The “amplification factor” A
, for our purposes, can be treated as a constant.
The objective of get_y()
The function get_y()
is used during a swap. Similar to k
in Uniswap V2, D
must be held constant during a swap (ignoring fees). Specifically, given a new value for x
, it computes the value of y
that keeps the equation balanced. Thus, it is an important subroutine for figuring out “if I put in this much token x
into the pool, how much token y
can be taken out?”
Curve can hold more than 2 tokens in the pool (e.g. 3pool holds USDT, USDC, and DAI). Curve identifies the coins by an index in an array. So, in this case, x
and y
refer to particular coins in that array. In this context, get_y()
means changing the balance of a particular token x
, holding the other balances constant, but allowing another token y
to change in value. Then, given a particular change in x
, compute how y
changes to keep the invariant balanced.
The invariant for n
tokens is:
$$
An^n\sum x_i +D=An^nD+\frac{D^{n+1}}{n^n\prod x_i}
$$
For simplicity, we will use $S$ instead of summation and $P$ instead of product in the rest of the article, so the invariant becomes:
$$
An^nS + D = ADn^n+\frac{D^{n+1}}{n^nP}
$$
Where $S$ is the sum of the balances of the tokens ($x_0 + x_1 + … + x_n$), $P$ is the product of the balances ($x_0x_1…x_n)$, and $x_i$ is the balance of token i
.
In the whitepaper, $S$ is written as $\sum x_i$ and $P$ is written as $\prod x_i$. The whitepaper equation is replicated below:
$$
An^n\sum x_i + D=ADn^n+\frac{D^{n+1}}{n^n\prod x_i}
$$
We will use $S$ and $P$ instead of the sum and product notation.
We assume that the pools can hold an arbitrary number $n$ tokens, so the formulas will reflect that. In practice, however, $n$ must be small, otherwise the $D^{n+1}$ term is liable to overflow.
Computing $D$ with get_D()
In get_D()
, we are presented with a set of balances x_0, x_1, ..., x_n
and we are to compute D
.
It is not possible to algebraically solve
$$
An^nS + D = ADn^n+\frac{D^{n+1}}{n^nP}
$$
for $D$. Instead, we need to apply Newton’s method to solve it numerically. To do so, we create a function $f(D)$, which is 0 when the equation is balanced.
$$
0 =ADn^n+\frac{D^{n+1}}{n^nP}-D-An^nS
$$
$$
0 =\frac{D^{n+1}}{n^nP}+ADn^n-D-An^nS
$$
$$
\color{green}{f(D)=\frac{D^{n+1}}{n^nP}+An^nD-D-An^nS}
$$
and we compute the derivative $f'(D)$ with respect to $D$ as:
$$
f'(D) = \frac{(n+1)D^n}{n^nP}+An^n-1
$$
Newton’s Method Formula
We can iteratively solve for $D$ using:
$$
D_\text{next}=D-\frac{f(D)}{f'(D)}
$$
It will be helpful to express $f'(D)$ with $D$ in the denominator. First we multiply the top and bottom of the left fraction defining $f'(D)$ by $D$.
$$
f'(D) = \frac{\frac{(n+1)D^{n+1}}{n^nP}}{D}+An^n-1
$$
And then combine $f'(D)$ into a single fraction:
$$
\begin{align*}
f'(D) &= \frac{\frac{(n+1)D^{n+1}}{n^nP}}{D}+\frac{(An^n-1)D}{D}\\f'(D) &=\color{red}{\frac{\frac{(n+1)D^{n+1}}{n^nP}+(An^n-1)D}{D}}
\end{align*}
$$
We can re-write Newton’s method to have a common denominator:
$$
\begin{align*}
D_\text{next}&=D-\frac{f(D)}{f'(D)}&&\text{// Newton’s Method}\\
&=D\frac{f'(D)}{f'(D)}-\frac{f(D)}
{f'(D)} &&\text{// multiply } D \text{ by }\frac{f'(D)}{f'(D)}\\
&=\frac{\color{violet}{D}\color{red}{f'(D)}-\color{green}{f(D)}}{\color{red}{f'(D)}}&&\text{// combine by common denominator}
\end{align*}
$$
By substituting the $f(D)$ and $f'(D)$ from earlier into the re-written Newton’s method formula we get:
$$
=\frac{\color{violet}{D}\color{red}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}-\color{green}{(\frac{D^{n+1}}{n^nP}+An^nD-D-An^nS)}}{\color{red}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}}
$$
Since we re-arranged $f'(D)$ to have $D$ in the denominator, the $\color{violet}D\color{red}f'(D)$ term will cancel nicely:
$$
=\frac{\color{violet}{\cancel{D}}\color{red}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{\cancel{D}}}-\color{green}{(\frac{D^{n+1}}{n^nP}+An^nD-D-An^nS)}}{\color{red}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}}
$$
$$
=\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D-\color{green}{(\frac{D^{n+1}}{n^nP}+An^nD-D-An^nS)}}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}
$$
Distribute all the terms to remove the parenthesis in the numerator:
$$
=\frac{\frac{D^{n+1}}{n^nP}+\frac{nD^{n+1}}{n^nP}+An^nD-D-\frac{D^{n+1}}{n^nP}-An^nD+D+An^nS}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}
$$
This leads to a lot of cancellations
$$
=\frac{\cancel{\frac{D^{n+1}}{n^nP}}+\frac{nD^{n+1}}{n^nP}+\cancel{An^nD}-\cancel{D}-\cancel{\frac{D^{n+1}}{n^nP}}-\cancel{An^nD}+\cancel{D}+An^nS}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}\\
$$
$$
=\frac{\frac{nD^{n+1}}{n^nP}+An^nS}{\frac{(n+1)\frac{D^{n+1}}{n^nP}+(An^n-1)D}{D}}
$$
We multiply the numerator and denominator by $D$
$$
=\frac{(An^nS+n{\frac{D^{n+1}}{n^nP}})D}{(An^n-1)D+(n+1){\frac{D^{n+1}}{n^nP}}}
$$
If we define $D_p$ as
$$
D_p=\frac{D^{n+1}}{n^nP}
$$
and substitute $D_p$ we get
$$
=\frac{(An^nS+n\boxed{\frac{D^{n+1}}{n^nP}})D}{(An^n-1)D+(n+1)\boxed{\frac{D^{n+1}}{n^nP}}}
$$
$$
D_\text{next}=\frac{(An^nS+D_pn)D}{(An^n-1)D+(n+1)D_p}
$$
Comparison to the original source code
This matches exactly what is in the Vyper code:
The variable $D_p$ was defined as:
D_P: uint256 = D # D_P = S
for _x in xp:
D_P = D_P * D / (_x * N_COINS)
xp
is the number of tokens, so the loop will run n
times. Therefore, we have $D$ multiplied by itself n
times in the denominator
$$
D_p=\frac{D^{n+1}}{n^n\prod_{i=1}^nx_i}
$$
Computing y
with get_y()
The idea is we force one of the $x_i$ to take on a new value (the code calls this x
) and calculate the correct value for another $x_j$ (where $i \neq j)$ such that the equation stays balanced. The balance of the other tokens remains unchanged. $x_j$ is referred to as $y$.
Although a StableSwap pool could have multiple tokens, it is only possible to exchange two of those tokens at a time using get_y()
.
Again, we have the same invariant
$$
An^nS + D = ADn^n+\frac{D^{n+1}}{n^nP}
$$
$D$, $A$, and $n$ are fixed, but we will be changing two of the values in $S$ and $P$
$$
\begin{align*}
S &= x_0+x_1+…+x_n\\
P &= x_0x_1…x_n
\end{align*}
$$
Therefore, we need to adjust the formula a bit, since $S$ and $P$ contain the values we are computing for.
- $S’$ will be the sum of all the balances except the new balance of token $x_i$ that we are trying to solve for
- $P$ will be the product of the balances of all the tokens, except for the one we are trying to solve for.
In other words,
$$
\begin{align*}
S &= S’+y \\
P &= P’y
\end{align*}
$$
To stay consistent with the code, we will call the token whose new balance we are trying to compute $y$.
The formula then becomes
$$
An^n(S’+y) + D = ADn^n+\frac{D^{n+1}}{n^nP’y}
$$
Again, we derive an $f(y)$ which is 0 when the equation is balanced, and its derivative with respect to y
$$
\begin{align*}f(y) &= \color{green}{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}\\f'(y)&=\color{red}{\frac{-D^{n+1}}{n^nP’y^2}-An^n}\end{align*}
$$
Here is the formula for Newton’s method again:
$$
y_\text{next}=y-\frac{f(y)}{f'(y)}
$$
After substituting $f(y)$ and $f'(y)$ into Newton’s method we get:
$$
y_\text{next}=y-\frac{\color{green}{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}}{\color{red}{\frac{-D^{n+1}}{n^nP’y^2}-An^n}}
$$
Take $-1$ out of the denominator
$$
y_\text{next}=y–1\cdot\frac{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
$$
y_\text{next}=y+\frac{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
Multiply $y$ (in the box below) to have a common denominator:
$$
y_\text{next}=\boxed{y}+\frac{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
$$
y_\text{next}=y\frac{\frac{D^{n+1}}{n^nP’y^2}+An^n}{\frac{D^{n+1}}{n^nP’y^2}+An^n}+\frac{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
Distribute $y$ in the left term
$$
y_\text{next}=y\boxed{\frac{\frac{D^{n+1}}{n^nP’y^2}+An^n}{\frac{D^{n+1}}{n^nP’y^2}+An^n}}+\frac{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^n(S’+y) – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
$$
y_\text{next}=\frac{\frac{D^{n+1}}{n^nP’y}+An^ny}{\frac{D^{n+1}}{n^nP’y^2}+An^n}+\frac{ADn^n+\frac{D^{n+1}}{n^nP’y}-An^nS’-An^ny – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
Combine the sums with a common denominator
$$
y_\text{next}=\frac{ADn^n+2\frac{D^{n+1}}{n^nP’y}-An^nS’ – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
Creating a substitution from the original invariant
It might seem like the equation cannot be simplified further, but if we revisit our original invariant
$$
An^n(S’+y) + D = ADn^n+\frac{D^{n+1}}{n^nP’y}
$$
We can solve for $ADn^n$ we get
$$
An^n(S’+y) + D = \boxed{ADn^n}+\frac{D^{n+1}}{n^nP’y}
$$
$$
An^n(S’+y) + D -\frac{D^{n+1}}{n^nP’y}= \boxed{ADn^n}
$$
$$
ADn^n=\boxed{-\frac{D^{n+1}}{n^nP’y}+An^n(S’+y) + D}
$$
And then if we substitute $ADn^n$ into the numerator in our latest formula for $y_\text{next}$, we get
$$
y_\text{next}=\frac{\boxed{ADn^n}+2\frac{D^{n+1}}{n^nP’y}-An^nS’ – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
$$
y_\text{next}=\frac{\boxed{(-\frac{D^{n+1}}{n^nP’y}+An^n(S’+y) + D)}+2\frac{D^{n+1}}{n^nP’y}-An^nS’ – D}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
A lot of cancellation happens
$$
y_\text{next}=\frac{-\frac{D^{n+1}}{n^nP’y}+\cancel{An^nS’}+An^ny + \cancel{D}+\cancel{2}\frac{D^{n+1}}{n^nP’y}-\cancel{An^nS’} – \cancel{D}}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
And we are left with a much smaller equation
$$
y_\text{next}=\frac{An^ny +\frac{D^{n+1}}{n^nP’y}}{\frac{D^{n+1}}{n^nP’y^2}+An^n}
$$
We multiply the top and bottom by $\frac{y}{An^n}$
$$
y_\text{next}=\frac{(An^ny +\frac{D^{n+1}}{n^nP’y})\frac{y}{An^n}}{(\frac{D^{n+1}}{n^nP’y^2}+An^n)\frac{y}{An^n}}
$$
$$
y_\text{next}=\frac{y^2 +\frac{D^{n+1}}{n^nP’An^n}}{\frac{D^{n+1}}{n^nP’y}\frac{1}{An^n}+y}
$$
Going back to our invariant, we can solve for the fractional term in the denominator:
$$
An^n(S’+y) + D = ADn^n+\boxed{\frac{D^{n+1}}{n^nP’y}}
$$
$$
\frac{D^{n+1}}{n^nP’y}=\boxed{An^n(S’+y) + D -ADn^n}
$$
Then we can substitute that into the equation for ynext:
$$
y_\text{next}=\frac{y^2 +\frac{D^{n+1}}{n^nP’An^n}}{\boxed{\frac{D^{n+1}}{n^nP’y}}\frac{1}{An^n}+y}
$$
$$
y_\text{next}=\frac{y^2 +\frac{D^{n+1}}{n^nP’An^n}}{\boxed{(An^n(S’+y) + D -ADn^n)}\frac{1}{An^n}+y}
$$
Then we can distribute $\frac{1}{An^n}$and simplify the denominator
$$
y_\text{next}=\frac{y^2 +\frac{D^{n+1}}{n^nP’An^n}}{((S’+y) + \frac{D}{An^n} -D)+y}
$$
$$
y_\text{next}=\frac{y^2 +\frac{D^{n+1}}{n^nP’An^n}}{(S’+y + \frac{D}{An^n} -D)+y}
$$
Simplify the denominator by removing the parenthesis and adding the two $y$ together
$$
y_\text{next}=\frac{y^2 +\frac{D^{n+1}}{n^nP’An^n}}{2y+S’ + \frac{D}{An^n} -D}
$$
In the original code, Curve defines additional variables:
$$
c = \frac{D^{n+1}}{n^nP’An^n}
$$
$$
b = S’ + \frac{D}{An^n}
$$
After substitution into the formula for $y_\text{next}$, we get:
$$
y_\text{next}=\frac{y^2 +\boxed{\frac{D^{n+1}}{n^nP’An^n}}}{2y+\boxed{S’ + \frac{D}{An^n}} -D}
$$
$$
y_\text{next}=\frac{y^2 +c}{2y+b -D}
$$
Comparison to the original source code
This matches the Curve code exactly, see the purple box below:
Mismatch between Ann and Anⁿ
Rather confusingly, the Curve whitepaper uses the invariant $An^n$ but the codebase uses $Ann$. That is, the codebase appears to be computing A * n * n
rather than A * n ** n
. The reason for this discrepancy is that the codebase stores $A$ as $An^{n-1}$. Since $n$ is fixed at deployment time, precomputing $n^{n-1}$ allows the code to avoid computing an exponent on-chain, which is more costly operation.
Summary
The core invariant of the Curve does not allow the variables $D$ or $x_i$ to be solved symbolically. Instead, the terms must be solved numerically.
One takeway from this exercise is that good algebraic manipulation is a very effective gas optimization technique. The Curve developers were able to compute Newton’s method formula that is much smaller than naively plugging in $f$ and its derivative and leaving it at that.
Citations and Acknowledgements
The following resources were consulted in writing this article:
StableSwap – efficient mechanism for Stablecoin liquidity, Michael Egorov, https://resources.curve.fi/pdf/curve-stableswap.pdf
Understanding the Curve AMM, Part -1: StableSwap Invariant, Atul Agarwal https://atulagarwal.dev/posts/curveamm/stableswap/
Curve Finance Discord, “chanho”
https://discord.com/channels/729808684359876718/729812922649542758/1126630568004698132
Curve – Code Explaind – get_y() | DeFi, Smart Contract Programmer https://www.youtube.com/watch?v=jAhKbxoeskQ