CryptopoolLPOracle
Source: CryptopoolLPOracle.vy on GitHub
Overview
CryptopoolLPOracle prices the liquidity provider (LP) token of a Curve pool. This page is for developers who read or monitor the price that a YieldBasis market uses. The oracle combines the LP token price from the pool with a crvUSD price aggregator. It returns the LP token price in aggregated USD per LP token. Aggregated USD is a USD value that uses the crvUSD price from the aggregator. The LEVAMM, the market's leveraged AMM (contract AMM), uses this value for its trades, its debt limits, and its position value.
Price computation
The oracle prices the LP token from pool state and then converts crvUSD to aggregated USD:
- Pool components: the oracle reads
POOL.virtual_price()andPOOL.price_scale()directly.virtual_priceis the pool's growth accounting per LP token.price_scaleis the pool's internal price. - Internal LP price:
lp_price_internal = 2 · virtual_price · sqrt(price_scale · 1e18) / 1e18. This is the Curve LP price formula for a 2-coin pool, expressed in pool units (numeraire =coins(0), that is, crvUSD). - crvUSD aggregation: A price aggregator (
AGG) returns the price of crvUSD in aggregated USD. - Final price:
price = lp_price_internal · AGG.price() / 1e18, the LP price in aggregated USD per LP token.
The oracle does not call POOL.lp_price(). It computes the LP price formula from the pool invariant locally, so a change to POOL.lp_price() does not change the oracle. The oracle still depends on price_scale, which a pool policy can steer. For the limits on a policy, read Pool policy and price_scale.
On markets 3 to 10, LT uses a related price for share values: _price_oracle() = CRYPTOPOOL.price_scale() · agg.price() / 1e18. This is the asset price, not the LP price. It also depends on price_scale. For the share price of LT, the market's vault contract, and the earlier LT blueprint of markets 0 to 2, read pricePerShare() in LT.
The following figure shows what moves price_scale:
On a Twocrypto v3.0.0 pool, the pool sets the target of price_scale in these ways:
- Swaps and liquidity operations update the pool's
last_pricesand its EMA price,price_oracle. POLICY.get_price_scale()returns the target. A return of 0 selectsprice_oracle. A pool with no policy, and a policy that is killed, also giveprice_oracle. On a pool that runs the oracle-report policy, a signed Pyth price report, delivered withdeliver_oracle_report, also moves the target.tweak_pricemovesprice_scaleone step toward the target. It moves only in the firsttweak_priceof a block, only whenvp_boosted > lp_xcp_profit, and only when the newvirtual_pricestays above 1.0 and at or abovelp_xcp_profit. The pool clamps the target toprice_oracle ± 20%and limits the step.- A
price_scalemove can lowervirtual_price, but never belowlp_xcp_profit.
The market price of the asset reaches only through a price_scale move. The following figure shows what reads price_scale, and where Fundamental Value and Redemption Value separate:
The two prices that read price_scale work in this way:
- The oracle computes
price()fromvirtual_price,price_scale, and the aggregator. The LEVAMM uses this price, , forx0, its quotes (get_p(),get_dy()), its safe-band checks, andvalue_oracle(). State-changing calls, such asexchangeand_deposit, read it withprice_w(). - LT prices the asset at
price_scaletimes the aggregator price._calculate_valuescombines this price withvalue_oracle()to givepricePerShare(), the Fundamental Value. preview_withdraw()sizes the share of the LEVAMM collateral and debt with the same oracle values. It then prices the exit withcalc_withdraw_fixed_outon the live pool balances, not at . The result is the Redemption Value, and the gap between the two values is TRD.- When
price_scalemoves,get_p()of the LEVAMM moves with . A gap betweenget_p()and the pool price opens a VirtualPool arbitrage, as the Rebalance flow shows.
The LT of markets 0 to 2 comes from an earlier blueprint and reads the pool's price_oracle() instead of price_scale().
Staleness and update cadence
The oracle stores no price and has no update transaction to miss, so it cannot go stale in the push-feed (heartbeat) sense. Every call recomputes from live chain state:
virtual_price/price_scale: the oracle reads them from the pool in the block of the call.price_scalechanges only when the pool rebalances. Only the first operation in a block that callstweak_pricecan rebalance. It rebalances only when the pool has enough profit. On pools with a policy, the policy sets the rebalance target. For the policy limits, read Pool policy and price_scale.AGG.price()is a view on the aggregator.price()uses it.AGG.price_w()is the state-changing form.price_w()uses it. Protocol calls that change state, such asAMM.exchange()andAMM._deposit(), callprice_w(). Routine protocol operations therefore update the aggregator state. No keeper is necessary.
This contract has no staleness check and no fallback feed. It uses the aggregator value as it is. The only check on the aggregator is the Factory band check , at Factory deployment and in set_agg. add_market does not check the band again, and routine operations do not check it. For the band check, read Factory.
AGG is immutable. An existing market keeps its aggregator. Factory.set_agg changes the aggregator only for markets created after the call.
Pool policy and price_scale
The oracle reads price_scale from the pool. On Twocrypto v3.0.0 pools, a policy contract can set the target toward which price_scale moves. The oracle has no check for this. It returns the price that results from the current price_scale. For the policy reference, read Pool policy.
The pool limits what a policy can do:
- The pool clamps the target to
price_oracle ± 20%. - Each rebalance moves
price_scaleby at most the pool's own step limit (adjustment_step_max). - Only the first operation in a block that calls
tweak_pricecan rebalance. It rebalances only when the profit gate passes.
Because lp_price = 2 · virtual_price · √price_scale, a 1% change in price_scale changes price() by about 0.5%. For example, if price_scale is 10% below price_oracle, price() is about 5% below the value at price_scale = price_oracle. LT pricePerShare() and the AMM safety limits use this lower value.
On a pool that runs the oracle-report policy, YBOraclizedPolicy, the policy moves the target toward a signed price report while the report is younger than the policy's report_lifetime. Otherwise it uses the pool's own prices. On a pool that runs YBTwocryptoPolicy, the target comes only from the pool's own prices. To see which policy a pool runs, call POLICY() on the pool or read the table in Contract addresses. The pools of markets 0 to 6 have no policy hook. Their price_scale follows only the native pool rule.
To monitor a market, read pool.POLICY(), pool.price_scale(), and pool.price_oracle(). Compare price_scale with price_oracle.
Calls from inside a pool operation
On v3.0.0 pools, price_scale() is @nonreentrant. price() and price_w() read price_scale() through lp_price(). So a contract that calls price() or price_w() from inside a pool operation gets a revert. On a policy pool, price_scale() does not call the policy.
Public variables
The oracle has these public variables.
Immutable variables
POOL(Cryptopool) - Curve pool contract address (immutable)AGG(PriceOracle) - Price aggregator contract address (immutable)
Functions
price()
Description: Returns the current LP token price in aggregated USD per LP token. It is a view function. It multiplies the internal LP price (crvUSD per LP token) by the aggregator price of crvUSD in aggregated USD.
Returns:
uint256 - LP token price in aggregated USD per LP token (scaled by 10^18).
Contract source: price()
The contract source gives this function body:
@external
@view
def price() -> uint256:
return self.lp_price() * staticcall AGG.price() // 10**18
price_w()
Description:
Returns the current LP token price in aggregated USD per LP token. It calls AGG.price_w(), which also updates the aggregator state. State-changing calls use it when they need a fresh price.
Returns:
uint256 - LP token price in aggregated USD per LP token (scaled by 10^18).
Contract source: price_w()
The contract source gives this function body:
@external
def price_w() -> uint256:
return self.lp_price() * extcall AGG.price_w() // 10**18
lp_price()
Description:
Internal helper that computes the LP token's value in pool-numeraire units (coins(0) = crvUSD) directly from virtual_price and price_scale. It reverts only if a pool view reverts. price() and price_w() call it. For the revert inside a pool operation, read Calls from inside a pool operation.
Contract source: lp_price()
The contract source in CryptopoolLPOracle.vy gives this function body:
@internal
@view
def lp_price() -> uint256:
virtual_price: uint256 = staticcall POOL.virtual_price()
p_scale: uint256 = staticcall POOL.price_scale()
return 2 * virtual_price * isqrt(p_scale * 10**18) // 10**18
Related
- LEVAMM: the contract that prices its trades, debt limits, and position value with this oracle.
- LT: the market's vault contract, and how
pricePerShare()uses the related asset price. - Oracle design: the user-level explanation of the oracle and its inputs.