A Brief Talk on Curve: The Most Powerful Stablecoin Exchange Protocol
Introduction
Introduction
StableSwap
first level title
StableSwap is a stable currency transaction model designed and proposed by Curve in the white paper, which can provide extremely low transaction slippage and unlimited liquidity.
secondary title

The constant function market maker curve (CFMM) of the StableSwap model is as follows:
The design concept is based on the fusion of two market-making models of constant sum and constant product, which has the characteristics of low slippage of constant sum and unlimited liquidity of constant product
secondary title

derive

Assuming that there are n stable coins in total, there are:

Combining the above two formulas to simplify can get the formula (1)
features
secondary title

Basepool
features
basepool is the basic exchange pool launched by Curve. The most common one is 3pool, which consists of three stable coins DAI, USDC, and USDT
The basepool contract implements the basic StableSwap transaction model
secondary title
@pure
@internal
def _get_D(_xp: uint256[N_COINS], _amp: uint256) -> uint256:
"""
D invariant calculation in non-overflowing integer operations
iteratively
A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
Converging solution:
D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1)
"""
S: uint256 = 0
Dprev: uint256 = 0
for _x in _xp:
S += _x
if S == 0:
return 0
D: uint256 = S
Ann: uint256 = _amp * N_COINS
for _i in range(255):
D_P: uint256 = D
for _x in _xp:
D_P = D_P * D / (_x * N_COINS) # If division by 0, this will be borked: only withdrawal will work. And that is good
Dprev = D
D = (Ann * S / A_PRECISION + D_P * N_COINS) * D / ((Ann - A_PRECISION) * D / A_PRECISION + (N_COINS + 1) *D_P)
# Equality with the precision of 1
if D > Dprev:
if D - Dprev <= 1:
return D
else:
if Dprev - D <= 1:
return D
# convergence typically occurs in 4 rounds or less, this should be unreachable!
# if it does happen the pool is borked and LPs can withdraw via `remove_liquidity`
raise
Parameter calculation
The same algorithm is also used in the _get_y function to calculate the y value
@view
@internal
def _get_y(i: int128, j: int128, x: uint256, _xp: uint256[N_COINS]) -> uint256:
"""
Calculate x[j] if one makes x[i] = x
Done by solving quadratic equation iteratively.
x_1**2 + x_1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)
x_1**2 + b*x_1 = c
x_1 = (x_1**2 + c) / (2*x_1 + b)
"""
# x in the input is converted to the same price/precision
assert i != j # dev: same coin
assert j >= 0 # dev: j below zero
assert j < N_COINS # dev: j above N_COINS
# should be unreachable, but good for safety
assert i >= 0
assert i < N_COINS
A: uint256 = self._A()
D: uint256 = self._get_D(_xp, A)
Ann: uint256 = A * N_COINS
c: uint256 = D
S: uint256 = 0
_x: uint256 = 0
y_prev: uint256 = 0
for _i in range(N_COINS):
if _i == i:
_x = x
elif _i != j:
_x = _xp[_i]
else:
continue
S += _x
c = c * D / (_x * N_COINS)
c = c * D * A_PRECISION / (Ann * N_COINS)
b: uint256 = S + D * A_PRECISION / Ann # - D
y: uint256 = D
for _i in range(255):
y_prev = y
y = (y*y + c) / (2 * y + b - D)
# Equality with the precision of 1
if y > y_prev:
if y - y_prev <= 1:
return y
else:
if y_prev - y <= 1:
return y
raise
fluidity
secondary title
@external
@nonreentrant('lock')
def add_liquidity(_amounts: uint256[N_COINS], _min_mint_amount: uint256) -> uint256:
"""
@notice Deposit coins into the pool
@param _amounts List of amounts of coins to deposit
@param _min_mint_amount Minimum amount of LP tokens to mint from the deposit
@return Amount of LP tokens received by depositing
"""
assert not self.is_killed # dev: is killed
amp: uint256 = self._A()
old_balances: uint256[N_COINS] = self.balances
# Initial invariant
D0: uint256 = self._get_D_mem(old_balances,fluidity
lp_token: address = self.lp_token
token_supply: uint256 = CurveToken(lp_token).totalSupply()
new_balances: uint256[N_COINS] = old_balances
for i in range(N_COINS):
if token_supply == 0:
assert _amounts[i] > 0 # dev: initial deposit requires all coins
# balances store amounts of c-tokens
new_balances[i] += _amounts[i]
# Invariant after change
D1: uint256 = self._get_D_mem(new_balances,The liquidity addition of the Curve pool is different from the two assets that Uniswap must add trading pairs, and the basepool can only add a certain asset in the pool
assert D1 > D0
# We need to recalculate the invariant accounting for fees
# to calculate fair user's share
D2: uint256 = D1
fees: uint256[N_COINS] = empty(uint256[N_COINS])
mint_amount: uint256 = 0
if token_supply >amp) #hunya# initial state D0
# Only account for fees if we are not the first to deposit
fee: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1))
admin_fee: uint256 = self.admin_fee
for i in range(N_COINS):
ideal_balance: uint256 = D1 * old_balances[i] / D0
difference: uint256 = 0
new_balance: uint256 = new_balances[i]
if ideal_balance > new_balance:
difference = ideal_balance - new_balance
else:
difference = new_balance - ideal_balance
fees[i] = fee * difference / FEE_DENOMINATOR
self.balances[i] = new_balance - (fees[i] * admin_fee / FEE_DENOMINATOR)
new_balances[i] -= fees[i]
D2 = self._get_D_mem(new_balances,amp) #hunya# theory D1 after adding liquidity
0: #hunya# It is not the first time to add liquidity to deduct the handling fee
else:
self.balances = new_balances
mint_amount = D1 # Take the dust if there was any
assert mint_amount >= _min_mint_amount, "Slippage screwed you"
# Take coins from the sender
for i in range(N_COINS):
if _amounts[i] > 0:
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
self.coins[i],
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(_amounts[i], bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransferFrom"
# Mint pool tokens
CurveToken(lp_token).mint(msg.sender, mint_amount)
log AddLiquidity(msg.sender, _amounts, fees, D1, token_supply + mint_amount)
return mint_amount
Metapool
amp) #hunya# D2 after deducting the handling fee

Metapool is a new pool derived from the expansion of basepool, which is used to anchor the new stable currency to the LP token of basepool. The liquidity provider of the basepool pool can add the LP token of the basepool to the metapool to add liquidity again, and further earn additional transaction fee income.
In the metapool contract, the new stable currency and the stable currency of the basic pool are indirectly coupled. While providing the exchange function between different stable coins, it also separates the risks of the new stable currency to a certain extent.
secondary title
@external
@nonreentrant('lock')
def exchange_underlying(i: int128, j: int128, _dx: uint256, _min_dy: uint256) -> uint256:
....
# Use base_i or base_j if they are >= 0
base_i: int128 = i - MAX_COIN
base_j: int128 = j - MAX_COIN
meta_i: int128 = MAX_COIN
meta_j: int128 = MAX_COIN
if base_i < 0:
meta_i = i
if base_j < 0:
meta_j = j
dy: uint256 = 0
...
Stablecoin exchange
old_balances: uint256[N_COINS] = self.balances
xp: uint256[N_COINS] = self._xp_mem(rates[MAX_COIN], old_balances)
x: uint256 = 0
The exchange_underlying function in the metapool contract realizes the mutual exchange function between stable coins
x = xp[i] + dx_w_fee * rates[i] / PRECISION
if base_i < 0 or base_j < 0: #hunya# swap includes meta pool tokens
# i is from BasePool
# At first, get the amount of pool tokens
base_inputs: uint256[BASE_N_COINS] = empty(uint256[BASE_N_COINS])
base_inputs[base_i] = dx_w_fee
coin_i: address = self.coins[MAX_COIN]
# Deposit and measure delta
x = ERC20(coin_i).balanceOf(self)
Curve(base_pool).add_liquidity(base_inputs,if base_i < 0: #hunya# input token is meta pool token
# Need to convert pool token to "virtual" units using rates
# dx is also different now
dx_w_fee = ERC20(coin_i).balanceOf(self) - x
x = dx_w_fee * rates[MAX_COIN] / PRECISION
# Adding number of pool tokens
x += xp[MAX_COIN]
...
# Withdraw from the base pool if needed
if base_j >else: #hunya# The input token is the base pool token
out_amount: uint256 = ERC20(output_coin).balanceOf(self)
Curve(base_pool).remove_liquidity_one_coin(dy, base_j,0) #hunya# base pool adds liquidity
dy = ERC20(output_coin).balanceOf(self) - out_amount
assert dy >= _min_dy, "Too few coins in result"
= 0: #hunya# output tokens are base pool tokens
# If both are from the base pool
dy = ERC20(output_coin).balanceOf(self)
Curve(base_pool).exchange(base_i, base_j, dx_w_fee, _min_dy)
dy = ERC20(output_coin).balanceOf(self) - dy
...
log TokenExchangeUnderlying(msg.sender, i, _dx, j, dy)
return dy
else: #hunya# Pure base pool token exchange, directly call the base pool exchange function
Summarize
If the exchange involves basepool tokens and metapool pool tokens, the intermediate process will be done by adding or removing liquidity through the LP of the basepool; if it is only the exchange of basepool tokens, the exchange function of the basepool will be called directly for exchange.

