Overview
As crvUSD markets use internal oracles, they utilize in-house liquidity pools to aggregate the price of collateral. But there is a possibility to use Chainlink oracle prices as safety limits.
CryptoWithStablePrice*.vyEvery market has its own price oracle contract, which can be fetched by calling price_oracle_contract within the controller of the market. The wstETH oracle will be used for the purpose of this documentation. Please be aware that oracle contracts can vary based on the collateral token.
The formulas below use slightly different terminologies than the code to make them easier to read. For abbreviations, see here.
CryptoWithStablePrice* Variants
The CryptoWithStablePrice oracle family is specifically designed for crvUSD mint markets. Each variant is tailored to a particular collateral type — the wstETH variant shown on this page is the most complex, using TVL-weighted tricrypto pools, staked ETH pricing, and optional Chainlink safety limits. Other variants are simpler, but all share the same core pattern: read price oracles from Curve pools, apply collateral-specific adjustments, and optionally bound with Chainlink.
| Variant | Collateral | Vyper | Pools | TVL Weighting |
|---|---|---|---|---|
CryptoWithStablePrice | sfrxETH | 0.3.10 | 2 tricrypto + 2 stableswap | Yes |
CryptoWithStablePriceTBTC | tBTC | 0.3.10 | 2 tricrypto + 2 stableswap | Yes |
CryptoWithStablePriceWstethN | wstETH | 0.3.10 | 2 tricrypto + 2 stableswap + staked swap | Yes |
CryptoWithStablePriceWBTC | WBTC | 0.3.10 | 2 tricrypto + 2 stableswap | Yes |
CryptoWithStablePriceAndChainlink | ETH | 0.3.10 | 1 tricrypto + 1 stableswap | No |
CryptoWithStablePriceFrxethN | sfrxETH v2 | 0.3.10 | 2 tricrypto + 2 stableswap + staked swap | Yes |
Not all crvUSD markets use CryptoWithStablePrice* oracles. Some markets use oracle contracts from the CryptoFromPool* family, which are shared with the lending system:
- CryptoFromPoolVaultWAgg — for ERC-4626 vault token collateral (e.g., sFRAX)
- CryptoFromPoolsRateWAgg — for collateral requiring multi-pool price chaining with rate adjustments
EMA of TVL
_ema_tvl() calculates the exponential moving average (EMA) of the total value locked (TVL) for TRICRYPTO pools.
This value is subsequently used in the internal function _raw_price() to compute the weighted price of ETH.
▶`_ema_tvl() -> uint256[N_POOLS]:`▼
last_timestamp: public(uint256)
last_tvl: public(uint256[N_POOLS])
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
@internal
@view
def _ema_tvl() -> uint256[N_POOLS]:
last_timestamp: uint256 = self.last_timestamp
last_tvl: uint256[N_POOLS] = self.last_tvl
if last_timestamp < block.timestamp:
alpha: uint256 = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
for i in range(N_POOLS):
tvl: uint256 = TRICRYPTO[i].totalSupply() * TRICRYPTO[i].virtual_price() / 10**18
last_tvl[i] = (tvl * (10**18 - alpha) + last_tvl[i] * alpha) / 10**18
return last_tvl
in TRICRYPTO[N_POOLS]
in TRICRYPTO[N_POOLS]
in TRICRYPTO[N_POOLS]
in TRICRYPTO[N_POOLS]
ema_tvl
Oracle.ema_tvl() -> uint256[N_POOLS]: viewFunction to calculate the Total-Value-Locked (TVL) Exponential-Moving-Average (EMA) of the TRICRYPTO pools.
Returns: last_tvl (uint256[N_POOLS]).
<>Source code▼
@external
@view
def ema_tvl() -> uint256[N_POOLS]:
return self._ema_tvl()
@internal
@view
def _ema_tvl() -> uint256[N_POOLS]:
last_timestamp: uint256 = self.last_timestamp
last_tvl: uint256[N_POOLS] = self.last_tvl
if last_timestamp < block.timestamp:
alpha: uint256 = self.exp(- convert((block.timestamp - last_timestamp) * 10**18 / TVL_MA_TIME, int256))
# alpha = 1.0 when dt = 0
# alpha = 0.0 when dt = inf
for i in range(N_POOLS):
tvl: uint256 = TRICRYPTO[i].totalSupply() * TRICRYPTO[i].virtual_price() / 10**18
last_tvl[i] = (tvl * (10**18 - alpha) + last_tvl[i] * alpha) / 10**18
return last_tvl
▶Example▼
>>> Oracle.ema_tvl()
38652775551183170655949, 40849321168337010409906
last_tvl
Oracle.last_tvl(arg0: uint256) -> uint256: viewGetter for the last_tvl of the tricrypto pool at index arg0.
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Index |
Returns: last tvl value (uint256).
<>Source code▼
last_tvl: public(uint256[N_POOLS])
▶Example▼
>>> Oracle.last_tvl(0)
38650114241563018578505
Calculate Raw Price
The internal _raw_price() function calculates the raw price of the collateral token.
▶`_raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:`▼
@internal
@view
def _raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:
weighted_price: uint256 = 0
weights: uint256 = 0
for i in range(N_POOLS):
p_crypto_r: uint256 = TRICRYPTO[i].price_oracle(TRICRYPTO_IX[i]) # d_usdt/d_eth
p_stable_r: uint256 = STABLESWAP[i].price_oracle() # d_usdt/d_st
p_stable_agg: uint256 = agg_price # d_usd/d_st
if IS_INVERSE[i]:
p_stable_r = 10**36 / p_stable_r
weight: uint256 = tvls[i]
# Prices are already EMA but weights - not so much
weights += weight
weighted_price += p_crypto_r * p_stable_agg / p_stable_r * weight # d_usd/d_eth
crv_p: uint256 = weighted_price / weights
use_chainlink: bool = self.use_chainlink
# Limit ETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
crv_p = min(max(crv_p, lower), upper)
p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth
# Limit STETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
p_staked = min(max(p_staked, lower), upper)
p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth
return p_staked * crv_p / 10**18
weighted price of ETH
total weighted price of ETH
price oracle of eth in the tricrypto pools w.r.t usdc/usdt
price oracle of stableswap pool
price oracle of crvusd
price of stETH w.r.t ETH
amount of stETH for 1 wstETH
raw_price
Oracle.raw_price() -> uint256: viewFunction to calculate the raw price.
Returns: raw price (uint256).
<>Source code▼
@external
@view
def raw_price() -> uint256:
return self._raw_price()
@internal
@view
def _raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:
weighted_price: uint256 = 0
weights: uint256 = 0
for i in range(N_POOLS):
p_crypto_r: uint256 = TRICRYPTO[i].price_oracle(TRICRYPTO_IX[i]) # d_usdt/d_eth
p_stable_r: uint256 = STABLESWAP[i].price_oracle() # d_usdt/d_st
p_stable_agg: uint256 = agg_price # d_usd/d_st
if IS_INVERSE[i]:
p_stable_r = 10**36 / p_stable_r
weight: uint256 = tvls[i]
# Prices are already EMA but weights - not so much
weights += weight
weighted_price += p_crypto_r * p_stable_agg / p_stable_r * weight # d_usd/d_eth
crv_p: uint256 = weighted_price / weights
use_chainlink: bool = self.use_chainlink
# Limit ETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
crv_p = min(max(crv_p, lower), upper)
p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth
# Limit STETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
p_staked = min(max(p_staked, lower), upper)
p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth
return p_staked * crv_p / 10**18
▶Example▼
>>> Oracle.raw_price()
1970446024043370547236
Chainlink Limits
The oracle contracts have the option to utilize Chainlink prices, which serve as safety limits. When enabled, these limits are triggered if the Chainlink price deviates by more than 1.5% (represented by BOUND_SIZE) from the internal price oracles.
Chainlink limits can be turned on and off by calling set_use_chainlink(do_it: bool), which can only be done by the admin of the Factory contract.

use_chainlink
Oracle.use_chainlink() -> bool: viewGetter method to check if chainlink oracles are turned on or off.
Returns: True or False (bool).
<>Source code▼
use_chainlink: public(bool)
▶Example▼
>>> Oracle.use_chainlink()
'False'
set_use_chainlink
Oracle.set_use_chainlink(do_it: bool)This function is only callable by the admin of the Factory contract.
Function to toggle the usage of chainlink limits.
| Input | Type | Description |
|---|---|---|
do_it | bool | Bool to toggle the usage of chainlink oracles |
<>Source code▼
use_chainlink: public(bool)
@external
def set_use_chainlink(do_it: bool):
assert msg.sender == FACTORY.admin()
self.use_chainlink = do_it
▶Example▼
>>> Oracle.set_use_chainlink('False')
Terminology used in Code
| terminology used in code | |
|---|---|
alpha | |
exp(power: int256) -> uint256: | |
TRICRYPTO[i].totalSupply() | |
TRICRYPTO[i].virtual_price() | |
p_crypto_r | |
p_stable_agg | |
p_stable_r | |
weighted_price | |
crv_p |
Contract Info Methods
N_POOLS
Oracle.N_POOLS() -> uint256: viewGetter for the number of external pools used by the oracle.
Returns: number of pools (uint256).
<>Source code▼
N_POOLS: public(constant(uint256)) = 2
▶Example▼
>>> Oracle.N_POOLS()
2
TRICRYPTO
Oracle.TRICRYPTO(arg0: uint256) -> address: viewGetter for the tricrypto pool at index arg0.
Returns: tricrypto pool address (address).
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Index |
<>Source code▼
TRICRYPTO: public(immutable(Tricrypto[N_POOLS]))
▶Example▼
>>> Oracle.TRICRYPTO(0)
'0x7F86Bf177Dd4F3494b841a37e810A34dD56c829B'
TRICRYPTO_IX
Oracle.TRICRYPTO_IX(arg0: uint256) -> uint256: viewGetter for the index of ETH in the tricrypto pool w.r.t the coin at index 0.
Returns: Index of ETH price oracle in the tricrypto pool (uint256).
Returns 1, as ETH price oracle index in the tricrypto pool is 1. If the same index would be 0, it would return the price oracle of ETH. Their prices are all w.r.t the coin at index 0 (USDC or USDT).
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Index of TRICRYPTO |
<>Source code▼
TRICRYPTO_IX: public(immutable(uint256[N_POOLS]))
▶Example▼
>>> Oracle.TRICRYPTO_IX(0)
1
STABLESWAP_AGGREGATOR
Oracle.STABLESWAP_AGGREGATOR() -> address: viewGetter for contract of the crvusd price aggregator.
Returns: contract (address).
<>Source code▼
STABLESWAP_AGGREGATOR: public(immutable(StableAggregator))
▶Example▼
>>> Oracle.STABLESWAP_AGGREGATOR()
'0x18672b1b0c623a30089A280Ed9256379fb0E4E62'
STABLESWAP
Oracle.STABLESWAP(arg0: uint256) -> address: viewGetter for the stableswap pool at index arg0.
Returns: stableswap pool (address).
| Input | Type | Description |
|---|---|---|
arg0 | uint256 | Index of STABLESWAP |
<>Source code▼
STABLESWAP: public(immutable(Stableswap[N_POOLS]))
▶Example▼
>>> Oracle.STABLESWAP(0)
'0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E'
STABLECOIN
Oracle.STABLECOIN() -> address: viewGetter for the contract address of crvUSD.
Returns: crvUSD contract (address).
<>Source code▼
STABLECOIN: public(immutable(address))
▶Example▼
>>> Oracle.STABLECOIN()
'0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E'
FACTORY
Oracle.FACTORY() -> address: viewGetter for the contract address of the Factory.
Returns: factory contract (address).
<>Source code▼
FACTORY: public(immutable(ControllerFactory))
▶Example▼
>>> Oracle.FACTORY()
'0xC9332fdCB1C491Dcc683bAe86Fe3cb70360738BC'
BOUND_SIZE
Oracle.BOUND_SIZE() -> uint256: viewGetter for the bound size of the chainlink oracle limits. This essentially is the size of the safety limits.
Returns: bound size (uint256).
<>Source code▼
BOUND_SIZE: public(immutable(uint256))
▶Example▼
>>> Oracle.BOUND_SIZE()
15000000000000000
STAKEDSWAP
Oracle.STAKEDSWAP() -> address: viewGetter for the stETH/ETH stableswap pool.
Returns: pool contract (address).
<>Source code▼
STAKEDSWAP: public(immutable(Stableswap))
▶Example▼
>>> Oracle.STAKEDSWAP()
'0x21E27a5E5513D6e65C4f830167390997aA84843a'
WSTETH
Oracle.WSTETH() -> address: viewGetter for the wstETH contract address.
Returns: wstETH contract (address).
<>Source code▼
WSTETH: public(immutable(wstETH))
▶Example▼
>>> Oracle.WSTETH()
'0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0'
last_timestamp
Oracle.last_timestamp() -> uint256: viewGetter for the last timestamp when price_w() was called.
Returns: timestamp (uint256).
<>Source code▼
last_timestamp: public(uint256)
▶Example▼
>>> Oracle.last_timestamp()
1692613703
TVL_MA_TIME
Oracle.TVL_MA_TIME() -> uint256: viewGetter for the Exponential-Moving-Average time.
Returns: ema time (uint256).
<>Source code▼
TVL_MA_TIME: public(constant(uint256)) = 50000 # s
▶Example▼
>>> Oracle.TVL_MA_TIME()
50000
price
Oracle.price() -> uint256: viewFunction to calculate the raw price of the collateral token.
Returns: raw price (uint256).
<>Source code▼
@external
@view
def price() -> uint256:
return self._raw_price(self._ema_tvl(), STABLESWAP_AGGREGATOR.price())
@internal
@view
def _raw_price(tvls: uint256[N_POOLS], agg_price: uint256) -> uint256:
weighted_price: uint256 = 0
weights: uint256 = 0
for i in range(N_POOLS):
p_crypto_r: uint256 = TRICRYPTO[i].price_oracle(TRICRYPTO_IX[i]) # d_usdt/d_eth
p_stable_r: uint256 = STABLESWAP[i].price_oracle() # d_usdt/d_st
p_stable_agg: uint256 = agg_price # d_usd/d_st
if IS_INVERSE[i]:
p_stable_r = 10**36 / p_stable_r
weight: uint256 = tvls[i]
# Prices are already EMA but weights - not so much
weights += weight
weighted_price += p_crypto_r * p_stable_agg / p_stable_r * weight # d_usd/d_eth
crv_p: uint256 = weighted_price / weights
use_chainlink: bool = self.use_chainlink
# Limit ETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_ETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_ETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
crv_p = min(max(crv_p, lower), upper)
p_staked: uint256 = STAKEDSWAP.price_oracle() # d_eth / d_steth
# Limit STETH price
if use_chainlink:
chainlink_lrd: ChainlinkAnswer = CHAINLINK_AGGREGATOR_STETH.latestRoundData()
if block.timestamp - min(chainlink_lrd.updated_at, block.timestamp) <= CHAINLINK_STALE_THRESHOLD:
chainlink_p: uint256 = convert(chainlink_lrd.answer, uint256) * 10**18 / CHAINLINK_PRICE_PRECISION_STETH
lower: uint256 = chainlink_p * (10**18 - BOUND_SIZE) / 10**18
upper: uint256 = chainlink_p * (10**18 + BOUND_SIZE) / 10**18
p_staked = min(max(p_staked, lower), upper)
p_staked = min(p_staked, 10**18) * WSTETH.stEthPerToken() / 10**18 # d_eth / d_wsteth
return p_staked * crv_p / 10**18
▶Example▼
>>> Oracle.price()
1970446024043370547236
price_w
Oracle.price_w() -> uint256Function to obtain the oracle price of the collateral token and update last_tvl and last_timestamp. This function is used in the AMM.
Returns: oracle price of the collateral token (uint256).
<>Source code▼
@external
def price_w() -> uint256:
tvls: uint256[N_POOLS] = self._ema_tvl()
if self.last_timestamp < block.timestamp:
self.last_timestamp = block.timestamp
self.last_tvl = tvls
return self._raw_price(tvls, STABLESWAP_AGGREGATOR.price_w())
▶Example▼
>>> Oracle.price_w()