Long time DeFi enjoyooooors should be familiar with the Curve ecosystem. Notorious for their 80s-era frontend, they enjoyed considerable success during the 2020-2021 bull run.
The original innovation from Curve’s founder Michael Egorov is the concept of the StableSwap liquidity pool, introduced in a 2019 whitepaper titled “StableSwap - efficient mechanism for Stablecoin liquidity”.
The StableSwap pool functions is built on an invariant, similar to Uniswap V2. An invariant is some controlling relationship that governs how prices and quantities are set before, during, and after a trade.
Uniswap’s invariant (the famous x·y=k) is known as a constant product. It is extremely simple to understand and implement. The generalized form of the constant product formula for n tokens looks like…
StableSwap’s invariant is slightly more complicated…
The big symbol on the left is a Sigma, which signifies that some number of elements are being added together. The big symbol on the right is a Pi, which signifies that some number of elements are being multiplied. The elements being multiplied typically have some subscript (i in this case) indicating that they are found in some fixed order.
∑xi signifies the “sum of all x”, ∏xi signifies the “product of all x”, where x represents a token amount in the pool. D represents the total quantity of tokens in the pool, n represents the number of tokens in the pool, and A is a multiplier applied to portions of this invariant.
StableSwap gives a special name to A, the amplification coefficient.
For A → 0, the invariant begins to behave as a constant product (Uniswap V2, Bancor, Balancer).
For A → ∞, the invariant begins to behave as a constant sum, which fixes prices along the complete liquidity range. No examples of this exist, because any pool with less-than-perfectly-equal token values would become totally drained in short order.
In short, StableSwap implements a simple method to control the “constant-ness” of the price behavior inside a pool of nominally equally-valued assets. A pool with high A will trade with very low slippage through the bulk of the liquidity range. Desirable for stablecoins, to be sure.
Math nerds will enjoy the Curve StableSwap explanation at Alvaro Fieto Boirac’s page very interesting, and I recommend checking it out.
Pool Types
Long time Uniswap users are familiar with the common WETH-[X] style liquidity pool. The need for a ubiquitous unit of exchange token is common across DEXes, since it is difficult to move between unrelated assets without a deeply liquid middleman token.
Curve has a concept of Base Pools, which are huge liquidity pools with deep reserves of commonly-held tokens. The first of these was the DAI-USDC-USDT, commonly called “3pool” or “TriPool”.
Curve provides another kind of pool called a Meta Pool, which is a StableSwap invariant pool that holds a pegged asset and LP tokens for a related base pool. This is a highly efficient method of allowing this 4th token to access the liquidity of all three tokens held in the base pool tokens without needing to create dilutive additional pools. For example, the GeminiUSD metapool at 0x4f062658EaAF2C1ccf8C8e36D6824CDf41167956 holds roughly $1.3M each of gUSD and 3CRV LP tokens.
With this 4th token + base pool approach, a user swapping between a meta pool and base pool asset uses the LP token as the common medium of exchange, instead of the ERC-20 tokens. On Curve’s interface, it simply appears as a pool with four tokens, the underlying LP tokens are otherwise invisible.
Refer to this example transaction where a user swapped USDT for gUSD via the metapool. The transaction required a transfer of 15,000 USDT into the 3pool, which transferred LP tokens to the metapool, which then transferred the equivalent gUSD to the user.
The other distinction, which is largely unrelated to the StableSwap invariant designation, is between Plain Pools and Lending Pools. A lending pool allows users on other platforms to borrow against the pool deposits, and liquidity providers earn interest from these loans.
You can read about all the Curve pools HERE. The other Curve pools (V2, StableSwap-NG, and beyond) which will be covered here in future articles.
Ecosystem Organization
The StableSwap pool ecosystem is organized on-chain through registry contracts. The umbrella contract is known as the Address Provider, and acts as a pointer to other contracts.
It is deployed at the address 0x0000000022D53366457F9d5E68Ec105046FC4383
on Ethereum, Arbitrum, Polygon, Fantom, Binance Smart Chain, and others.
Console Exploration
Let’s use Ape to poke around the ecosystem. Refer to my introduction from last month to learn about setting up Ape.
Launch an Ape console connected to mainnet:
(ape) btd@main:~$ ape console
INFO: Connecting to existing Geth node at http://localhost:8545/[hidden].
In [1]: address_provider = Contract(
'0x0000000022D53366457F9d5E68Ec105046FC4383'
)
In [2]: address_provider.max_id()
Out[2]: 11
The registry is tracking 12 contracts (since the index starts at zero). You can view them one-by-one by supplying the ID to get_id_info()
:
In [3]: address_provider.get_id_info(0)
Out[3]: get_id_info_return(
addr='0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5',
is_active=True,
version=4,
last_modified=1633815119,
description='Main Registry'
)
Here is the current list:
ID 0:
addr='0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5'
description='Main Registry'
ID 1:
addr='0xe64608E223433E8a03a1DaaeFD8Cb638C14B552C'
description='PoolInfo Getters'
ID 2:
addr='0x99a58482BD75cbab83b27EC03CA68fF489b5788f'
description='Exchanges'
ID 3:
addr='0xB9fC157394Af804a3578134A6585C0dc9cc990d4'
description='Metapool Factory'
ID 4:
addr='0xA464e6DCda8AC41e03616F95f4BC98a13b8922Dc',
description='Fee Distributor'
ID 5:
addr='0x8F942C20D02bEfc377D41445793068908E2250D0'
description='CryptoSwap Registry'
ID 6:
addr='0xF18056Bbd320E96A48e3Fbf8bC061322531aac99'
description='Cryptopool Factory'
ID 7:
addr='0xF98B45FA17DE75FB1aD0e7aFD971b0ca00e379fC'
description='Metaregistry'
ID 8:
addr='0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d'
description='crvUSD plain pools'
ID 9:
addr='0x0000000000000000000000000000000000000000'
is_active=False
description='crvUSD plain pools'
ID 10:
addr='0x0000000000000000000000000000000000000000'
is_active=False
description='crvUSD plain pools'
ID 11:
addr='0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963'
description='Curve Tricrypto Factory'
Notice that IDs 9 and 10 are marked with is_active=False
, so not all contract IDs that appear in the address provider will be used. A used ID will never be re-assigned, but the address associated with a given ID might change as contracts are replaced.
The contract at ID 0 is the main registry, which we can use to retrieve information about custom StableSwap pools deployed by Curve:
In [4]: registry = Contract(
'0x90E00ACe148ca3b23Ac1bC8C240C2a7Dd9c2d7f5'
)
In [5]: registry.pool_count()
Out[5]: 49
There are currently 49 Curve-deployed pools. Information for each pool can be retrieved using pool_list
with the pool ID:
In [6]: registry.pool_list(0)
Out[6]: '0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7'
Going to Etherscan, we find this is the famous tricrypto DAI-USDC-USDT pool.
I mentioned above that the main registry contract is used to track the custom pools deployed directly by Curve. The unfortunate consequence of having a template-based rollout is that most of the custom pools have subtle differences that makes working with them quite difficult.
The closest thing I’ve found to a standard contract is SwapTemplateBase.vy on the Curve Github.
StableSwap Factory
Curve allows users to deploy StableSwap pools through a factory contract. These are deployed with a forwarding proxy, and have a common ABI. The address is stored at position 3 in the address provider.
In [1]: provider = Contract(
'0x0000000022D53366457F9d5E68Ec105046FC4383'
)
In [2]: stableswap_factory = Contract(provider.get_address(3))
In [3]: stableswap_factory.pool_count()
Out[3]: 375
There are currently 375 StableSwap pools, and you can get their addresses in the same way as before:
In [4]: stableswap_factory.pool_list(0)
Out[4]: '0x1F71f05CF491595652378Fe94B7820344A551B8E'
In [5]: stableswap_factory.pool_list(1)
Out[5]: '0xFD5dB7463a3aB53fD211b4af195c5BCCC1A03890'
In [6]: stableswap_factory.pool_list(2)
Out[6]: '0x8461A004b50d321CB22B7d034969cE6803911899'
Swap Calculation
The Vyper contract handles the swap calculation by solving for the amount removed from the pool for a given input using Newton’s method for solving quadratic equations.