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:
There are two frequent math operations we wish to conduct with it:
Compute given fixed values for , and the reserves . Note that , 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 , we wish to increase the value of one of the reserves to a new value and figure out how much another reserve needs to decrease to keep the equation balanced. This is what the function get_y() does. Here “y” means .
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
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:
For simplicity, we will use instead of summation and instead of product in the rest of the article, so the invariant becomes:
Where is the sum of the balances of the tokens (), is the product of the balances (, and is the balance of token i.
In the whitepaper, is written as and is written as . The whitepaper equation is replicated below:
We will use and instead of the sum and product notation.
We assume that the pools can hold an arbitrary number tokens, so the formulas will reflect that. In practice, however, must be small, otherwise the term is liable to overflow.
Computing 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
for . Instead, we need to apply Newton’s method to solve it numerically. To do so, we create a function , which is 0 when the equation is balanced.
and we compute the derivative with respect to as:
Newton’s Method Formula
We can iteratively solve for using:
It will be helpful to express with in the denominator. First we multiply the top and bottom of the left fraction defining by .
And then combine into a single fraction:
We can re-write Newton’s method to have a common denominator:
By substituting the and from earlier into the re-written Newton’s method formula we get:
Since we re-arranged to have in the denominator, the term will cancel nicely:
Distribute all the terms to remove the parenthesis in the numerator:
D_P: uint256 = D # D_P = Sfor _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 multiplied by itself n times in the denominator
Computing y with get_y()
The idea is we force one of the to take on a new value (the code calls this x) and calculate the correct value for another (where such that the equation stays balanced. The balance of the other tokens remains unchanged. is referred to as .
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
, , and are fixed, but we will be changing two of the values in and
Therefore, we need to adjust the formula a bit, since and contain the values we are computing for.
will be the sum of all the balances except the new balance of token that we are trying to solve for
will be the product of the balances of all the tokens, except for the one we are trying to solve for.
In other words,
To stay consistent with the code, we will call the token whose new balance we are trying to compute .
The formula then becomes
Again, we derive an which is 0 when the equation is balanced, and its derivative with respect to y
Here is the formula for Newton’s method again:
After substituting and into Newton’s method we get:
Take out of the denominator
Multiply (in the box below) to have a common denominator:
Distribute in the left term
Combine the sums with a common denominator
Creating a substitution from the original invariant
It might seem like the equation cannot be simplified further, but if we revisit our original invariant
We can solve for we get
And then if we substitute into the numerator in our latest formula for , we get
A lot of cancellation happens
And we are left with a much smaller equation
We multiply the top and bottom by
Going back to our invariant, we can solve for the fractional term in the denominator:
Then we can substitute that into the equation for ynext:
Then we can distribute and simplify the denominator
Simplify the denominator by removing the parenthesis and adding the two together
In the original code, Curve defines additional variables:
After substitution into the formula for , we get:
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 but the codebase uses . 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 as . Since is fixed at deployment time, precomputing 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 or 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 and its derivative and leaving it at that.
Citations and Acknowledgements
The following resources were consulted in writing this article: