Metapools
A metapool is a pool where a stablecoin is paired against the LP token from another pool, a so-called base pool.
For example, a liquidity provider may deposit DAI
into 3Pool and in exchange receive the pool’s LP token 3CRV
. The 3CRV
LP token may then be deposited into the GUSD metapool, which contains the coins GUSD
and 3CRV
, in exchange for the metapool’s LP token gusd3CRV. The obtained LP token may then be staked in the metapool’s liquidity gauge for CRV
rewards.
Metapools provide an opportunity for the base pool liquidity providers to earn additional trading fees by depositing their LP tokens into the metapool. Note that the CRV
rewards received for staking LP tokens into the pool’s liquidity gauge may differ for the base pool’s liquidity gauge and the metapool’s liquidity gauge. For details on liquidity gauges and protocol rewards, please refer to Liquidity Gauges and Minting CRV.
Note
Metapools also implement the ABI from plain pools. The template source code for metapools may be viewed on GitHub.
Pool Info Methods¶
base_coins
¶
StableSwap.base_coins(i: uint256) → address: view
Get the coins of the base pool. Returns address
of the coin at index i
.
Input | Type | Description |
---|---|---|
i | uint256 | Coin index |
Source code
# Token corresponding to the pool is always the last one
BASE_POOL_COINS: constant(int128) = 3
...
base_coins: public(address[BASE_POOL_COINS])
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
StableSwap.coins
¶
StableSwap.coins(i: uint256) → address: view
Get the coins of the metapool. Returns address
of coin at index i
.
Input | Type | Description |
---|---|---|
i | uint256 | Coin index |
Source code
N_COINS: constant(int128) = 2
...
coins: public(address[N_COINS])
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
StableSwap.base_pool
¶
StableSwap.base_pool() → address: view
Get the address of the base pool. Returns address
of the base pool implementation.
Source code
base_pool: public(address)
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
StableSwap.base_virtual_price
¶
StableSwap.base_virtual_price() → uint256: view
Get the current price of the base pool LP token relative to the underlying base pool assets.
Source code
base_virtual_price: public(uint256)
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
Note
The base pool’s virtual price is only fetched from the base pool if the cached price has expired. A fetched based pool virtual price is cached for 10 minutes (BASE_CACHE_EXPIRES: constant(int128) = 10 * 60
).
StableSwap.base_cache_update()
¶
StableSwap.base_cache_update() → uint256: view
Get the timestamp at which the base pool virtual price was last cached.
Source code
base_cache_updated: public(uint256)
...
BASE_CACHE_EXPIRES: constant(int128) = 10 * 60 # 10 min
...
@external
def __init__(
_owner: address,
_coins: address[N_COINS],
_pool_token: address,
_base_pool: address,
_A: uint256,
_fee: uint256,
_admin_fee: uint256
):
"""
@notice Contract constructor
@param _owner Contract owner address
@param _coins Addresses of ERC20 conracts of coins
@param _pool_token Address of the token representing LP share
@param _base_pool Address of the base pool (which will have a virtual price)
@param _A Amplification coefficient multiplied by n * (n - 1)
@param _fee Fee to charge for exchanges
@param _admin_fee Admin fee
"""
for i in range(N_COINS):
assert _coins[i] != ZERO_ADDRESS
self.coins = _coins
self.initial_A = _A * A_PRECISION
self.future_A = _A * A_PRECISION
self.fee = _fee
self.admin_fee = _admin_fee
self.owner = _owner
self.kill_deadline = block.timestamp + KILL_DEADLINE_DT
self.token = CurveToken(_pool_token)
self.base_pool = _base_pool
self.base_virtual_price = Curve(_base_pool).get_virtual_price()
self.base_cache_updated = block.timestamp
for i in range(BASE_POOL_COINS):
_base_coin: address = Curve(_base_pool).coins(convert(i, uint256))
self.base_coins[i] = _base_coin
# approve underlying coins for infinite transfers
_response: Bytes[32] = raw_call(
_base_coin,
concat(
method_id("approve(address,uint256)"),
convert(_base_pool, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(_response) > 0:
assert convert(_response, bool)
...
@internal
def _vp_rate() -> uint256:
if block.timestamp > self.base_cache_updated + BASE_CACHE_EXPIRES:
vprice: uint256 = Curve(self.base_pool).get_virtual_price()
self.base_virtual_price = vprice
self.base_cache_updated = block.timestamp
return vprice
else:
return self.base_virtual_price
@internal
@view
def _vp_rate_ro() -> uint256:
if block.timestamp > self.base_cache_updated + BASE_CACHE_EXPIRES:
return Curve(self.base_pool).get_virtual_price()
else:
return self.base_virtual_price
Exchange Methods¶
Similar to lending pools, on metapools exchanges can be made either between the coins the metapool actually holds (another pool’s LP token and some other coin) or between the metapool’s underlying coins. In the context of a metapool, underlying coins refers to the metapool’s coin and any of the base pool’s coins. The base pool’s LP token is not included as an underlying coin.
For example, the GUSD metapool would have the following:
Coins: GUSD
, 3CRV
(3Pool LP)
Underlying coins: GUSD
, DAI
, USDC
, USDT
Note
While metapools contain public getters for coins
and base_coins
, there exists no getter for obtaining a list of all underlying coins.
StableSwap.exchange
¶
StableSwap.exchange(i: int128, j: int128, _dx: uint256, _min_dy: uint256) → uint256
Perform an exchange between two (non-underlying) coins in the metapool. Index values can be found via the coins
public getter method.
Returns: the actual amount of coin j
received.
Input | Type | Description |
---|---|---|
i | int128 | Index value for the coin to send |
j | int128 | Index value of the coin to receive |
_dx | uint256 | Amount of i being exchanged |
_min_dy | uint256 | Minimum amount of j to receive |
Emits: TokenExchange
todo: explain how fee is calculated
Source code
@external
@nonreentrant('lock')
def exchange(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two coins
@dev Index values can be found via the `coins` public getter method
@param i Index value for the coin to send
@param j Index valie of the coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not self.is_killed # dev: is killed
rates: uint256[N_COINS] = RATES
rates[MAX_COIN] = self._vp_rate()
old_balances: uint256[N_COINS] = self.balances
xp: uint256[N_COINS] = self._xp_mem(rates[MAX_COIN], old_balances)
x: uint256 = xp[i] + dx * rates[i] / PRECISION
y: uint256 = self.get_y(i, j, x, xp)
dy: uint256 = xp[j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR
# Convert all to real units
dy = (dy - dy_fee) * PRECISION / rates[j]
assert dy >= min_dy, "Too few coins in result"
dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR
dy_admin_fee = dy_admin_fee * PRECISION / rates[j]
# Change balances exactly in same way as we change actual ERC20 coin amounts
self.balances[i] = old_balances[i] + dx
# When rounding errors happen, we undercharge admin fee in favor of LP
self.balances[j] = old_balances[j] - dy - dy_admin_fee
assert ERC20(self.coins[i]).transferFrom(msg.sender, self, dx)
assert ERC20(self.coins[j]).transfer(msg.sender, dy)
log TokenExchange(msg.sender, i, dx, j, dy)
return dy
StableSwap.exchange_underlying
¶
StableSwap.exchange_underlying(i: int128, j: int128, _dx: uint256, _min_dy: uint256) → uint256
Perform an exchange between two underlying tokens. Index values are the coins
followed by the base_coins
, where the base pool LP token is not included as a value.
Returns: the actual amount of coin j
received.
Input | Type | Description |
---|---|---|
i | int128 | Index value for the coin to send |
j | int128 | Index value of the coin to receive |
_dx | uint256 | Amount of i being exchanged |
_min_dy | uint256 | Minimum amount of j to receive |
Emits: TokenExchangeUnderlying
Source code
@external
@nonreentrant('lock')
def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
"""
@notice Perform an exchange between two underlying coins
@dev Index values can be found via the `underlying_coins` public getter method
@param i Index value for the underlying coin to send
@param j Index valie of the underlying coin to recieve
@param dx Amount of `i` being exchanged
@param min_dy Minimum amount of `j` to receive
@return Actual amount of `j` received
"""
assert not self.is_killed # dev: is killed
rates: uint256[N_COINS] = RATES
rates[MAX_COIN] = self._vp_rate()
_base_pool: address = self.base_pool
# 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
# Addresses for input and output coins
input_coin: address = ZERO_ADDRESS
if base_i < 0:
input_coin = self.coins[i]
else:
input_coin = self.base_coins[base_i]
output_coin: address = ZERO_ADDRESS
if base_j < 0:
output_coin = self.coins[j]
else:
output_coin = self.base_coins[base_j]
# Handle potential Tether fees
dx_w_fee: uint256 = dx
if input_coin == FEE_ASSET:
dx_w_fee = ERC20(FEE_ASSET).balanceOf(self)
# "safeTransferFrom" which works for ERC20s which return bool or not
_response: Bytes[32] = raw_call(
input_coin,
concat(
method_id("transferFrom(address,address,uint256)"),
convert(msg.sender, bytes32),
convert(self, bytes32),
convert(dx, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransferFrom"
# Handle potential Tether fees
if input_coin == FEE_ASSET:
dx_w_fee = ERC20(FEE_ASSET).balanceOf(self) - dx_w_fee
if base_i < 0 or base_j < 0:
old_balances: uint256[N_COINS] = self.balances
xp: uint256[N_COINS] = self._xp_mem(rates[MAX_COIN], old_balances)
x: uint256 = 0
if base_i < 0:
x = xp[i] + dx_w_fee * rates[i] / PRECISION
else:
# 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, 0)
# 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]
y: uint256 = self.get_y(meta_i, meta_j, x, xp)
# Either a real coin or token
dy = xp[meta_j] - y - 1 # -1 just in case there were some rounding errors
dy_fee: uint256 = dy * self.fee / FEE_DENOMINATOR
# Convert all to real units
# Works for both pool coins and real coins
dy = (dy - dy_fee) * PRECISION / rates[meta_j]
dy_admin_fee: uint256 = dy_fee * self.admin_fee / FEE_DENOMINATOR
dy_admin_fee = dy_admin_fee * PRECISION / rates[meta_j]
# Change balances exactly in same way as we change actual ERC20 coin amounts
self.balances[meta_i] = old_balances[meta_i] + dx_w_fee
# When rounding errors happen, we undercharge admin fee in favor of LP
self.balances[meta_j] = old_balances[meta_j] - dy - dy_admin_fee
# Withdraw from the base pool if needed
if base_j >= 0:
out_amount: uint256 = ERC20(output_coin).balanceOf(self)
Curve(_base_pool).remove_liquidity_one_coin(dy, base_j, 0)
dy = ERC20(output_coin).balanceOf(self) - out_amount
assert dy >= min_dy, "Too few coins in result"
else:
# 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
# "safeTransfer" which works for ERC20s which return bool or not
_response = raw_call(
output_coin,
concat(
method_id("transfer(address,uint256)"),
convert(msg.sender, bytes32),
convert(dy, bytes32),
),
max_outsize=32,
) # dev: failed transfer
if len(_response) > 0:
assert convert(_response, bool) # dev: failed transfer
# end "safeTransfer"
log TokenExchangeUnderlying(msg.sender, i, dx, j, dy)
return dy