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:
Ann∑xi+D=AnnD+nn∏xiDn+1
There are two frequent math operations we wish to conduct with it:
Compute D given fixed values for A, and the reserves x1,…,xn. 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 xi to a new value xi′ and figure out how much another reserve xj needs to decrease to keep the equation balanced. This is what the function get_y() does. Here “y” means xi′.
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+4xyD3
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:
Ann∑xi+D=AnnD+nn∏xiDn+1
For simplicity, we will use S instead of summation and P instead of product in the rest of the article, so the invariant becomes:
AnnS+D=ADnn+nnPDn+1
Where S is the sum of the balances of the tokens (x0+x1+…+xn), P is the product of the balances (x0x1...xn), and xi is the balance of token i.
In the whitepaper, S is written as ∑xi and P is written as ∏xi. The whitepaper equation is replicated below:
Ann∑xi+D=ADnn+nn∏xiDn+1
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 Dn+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
AnnS+D=ADnn+nnPDn+1
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=ADnn+nnPDn+1−D−AnnS
0=nnPDn+1+ADnn−D−AnnS
f(D)=nnPDn+1+AnnD−D−AnnS
and we compute the derivative f′(D) with respect to D as:
f′(D)=nnP(n+1)Dn+Ann−1
Newton’s Method Formula
We can iteratively solve for D using:
Dnext=D−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.
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
Dp=nn∏i=1nxiDn+1
Computing y with get_y()
The idea is we force one of the xi to take on a new value (the code calls this x) and calculate the correct value for another xj (where i=j) such that the equation stays balanced. The balance of the other tokens remains unchanged. xj 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
AnnS+D=ADnn+nnPDn+1
D, A, and n are fixed, but we will be changing two of the values in S and P
SP=x0+x1+...+xn=x0x1...xn
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 xi 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,
SP=S′+y=P′y
To stay consistent with the code, we will call the token whose new balance we are trying to compute y.
The formula then becomes
Ann(S′+y)+D=ADnn+nnP′yDn+1
Again, we derive an f(y) which is 0 when the equation is balanced, and its derivative with respect to y
Going back to our invariant, we can solve for the fractional term in the denominator:
Ann(S′+y)+D=ADnn+nnP′yDn+1
nnP′yDn+1=Ann(S′+y)+D−ADnn
Then we can substitute that into the equation for ynext:
ynext=nnP′yDn+1Ann1+yy2+nnP′AnnDn+1
ynext=(Ann(S′+y)+D−ADnn)Ann1+yy2+nnP′AnnDn+1
Then we can distribute Ann1and simplify the denominator
ynext=((S′+y)+AnnD−D)+yy2+nnP′AnnDn+1
ynext=(S′+y+AnnD−D)+yy2+nnP′AnnDn+1
Simplify the denominator by removing the parenthesis and adding the two y together
ynext=2y+S′+AnnD−Dy2+nnP′AnnDn+1
In the original code, Curve defines additional variables:
c=nnP′AnnDn+1
b=S′+AnnD
After substitution into the formula for ynext, we get:
ynext=2y+S′+AnnD−Dy2+nnP′AnnDn+1
ynext=2y+b−Dy2+c
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 Ann 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 Ann−1. Since n is fixed at deployment time, precomputing nn−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 xi 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: