Curve StableSwap Liquidity Pool — Part II: Extending The Helper
Meta Pools and Base Pools and Lending Pools, Oh My!
It’s taken longer than I expected to get to this point, but I’m pleased to report that the CurveStableswapPool
helper introduced in Part I is now fairly sophisticated.
When I started this Curve series, I thought it would be straightforward. The StableSwap invariant from the white paper is fairly simple to figure out, and the base templates from Github suggest that the platform is stable and can be explored.
After publishing, I started writing tests and looking to verify the offchain calculations against the onchain calculations. The first pool worked, but the second pool needed some customization. Then the third pool needed some customization that broke the first pool, requiring some special handling. Etc etc.
I went into the tank for a while, fixing and cleaning up the various quirks required to support each and every Curve Stableswap pool.
Over the past few weeks I’ve been pushing a lot of changes to the curve
feature branch on the degenbot Github.
There is a lot of technical debt associated with the early Curve ecosystem. Differing compiler versions, custom contracts, and multiple registries make it quite challenging to develop a clean abstraction for these pools.
But against all odds, autism has prevailed and the helper is mostly ready to build on.
It is roughly 2000 lines of code, most of which is special handling for the differint Curve pools out in the wild.
If you see this thing for the first time without context, it will make zero sense. So I’m going to do a quick tour of how it all works.
Solving For Stability
The StableSwap invariant is preserved on-chain using a simple method.
To highlight, let’s review an extremely common calculation: For some pool, when an amount (dx) of token i is deposited, determine the maximum amount of token j that can be withdrawn (dy) while satisfying the invariant.
The calculation is made in steps:
Determine the value of xp, an ordered array of the decimal-corrected balances for all tokens held by the pool prior to the swap.
Determine the value of x, a balance of token i after the deposit dx.
Determine the initial value of D, the arithmetic mean of decimal-corrected balances held by xp
Determine the greatest possible value of y, a balance of token j that satisfies the invariant constraint at the new balance of x, such that D' (at the new balance) is within some amount of the original.
Subtract the fee from the difference of dy and y, and send the remainder to the user.
This is highly simplified, of course, and it omits a lot of detail that I encourage you to read in the Curve Stableswap: From Whitepaper to Vyper whitepaper, or in the very detailed book “Automated Market Makers” by Miguel Ottina et al.
Step 4 as implemented by a Curve pool contract involves repeated use of Newton’s Method to identify the root of the quadratic equation. Both y and D are found this way.
Equations: Not Always Equal?
If that was all we had to do, working with Curve pools would be very simple. Follow the steps above and we’re done! There would be some logic involved to determine the decimal corrections and dynamically adjust the pool parameters (amplification coefficient, fee, number of tokens, etc), but that’s all simple enough.
Where you’ll run into trouble with Curve is that some equations are implemented differently.
For example, let’s take a section of get_D()
implemented by two contracts.