Skip to main content

CryptopoolLPOracle

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:

  1. Pool components: the oracle reads POOL.virtual_price() and POOL.price_scale() directly. virtual_price is the pool's growth accounting per LP token. price_scale is the pool's internal price.
  2. 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).
  3. crvUSD aggregation: A price aggregator (AGG) returns the price of crvUSD in aggregated USD.
  4. 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:

What moves price_scale: pool swaps and liquidity operations update the pool's last prices and price_oracle, and on a pool that runs the oracle-report policy a signed Pyth price report also feeds the policy. POLICY.get_price_scale() gives the target, or price_oracle when it returns 0. tweak_price moves price_scale toward the target only in the first tweak_price of a block, past the profit gate, within price_oracle ± 20% and the step limits, and only if the new virtual_price stays above 1.0 and at or above lp_xcp_profit.What moves price_scale: pool swaps and liquidity operations update the pool's last prices and price_oracle, and on a pool that runs the oracle-report policy a signed Pyth price report also feeds the policy. POLICY.get_price_scale() gives the target, or price_oracle when it returns 0. tweak_price moves price_scale toward the target only in the first tweak_price of a block, past the profit gate, within price_oracle ± 20% and the step limits, and only if the new virtual_price stays above 1.0 and at or above lp_xcp_profit.

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_prices and its EMA price, price_oracle.
  • POLICY.get_price_scale() returns the target. A return of 0 selects price_oracle. A pool with no policy, and a policy that is killed, also give price_oracle. On a pool that runs the oracle-report policy, a signed Pyth price report, delivered with deliver_oracle_report, also moves the target.
  • tweak_price moves price_scale one step toward the target. It moves only in the first tweak_price of a block, only when vp_boosted > lp_xcp_profit, and only when the new virtual_price stays above 1.0 and at or above lp_xcp_profit. The pool clamps the target to price_oracle ± 20% and limits the step.
  • A price_scale move can lower virtual_price, but never below lp_xcp_profit.

The market price of the asset reaches pop_o only through a price_scale move. The following figure shows what reads price_scale, and where Fundamental Value and Redemption Value separate:

What reads price_scale: CryptopoolLPOracle prices the LP from virtual_price, price_scale, and the crvUSD aggregator, and the LEVAMM uses that price, p_o, for x0, quotes, safe-band checks, and value_oracle(). LT prices a share from value_oracle() and price_scale times the crvUSD price, which gives pricePerShare(), the Fundamental Value. preview_withdraw() sizes the share of LEVAMM collateral and debt with oracle values, then prices the exit from live pool balances, which gives the Redemption Value. The gap between the two is TRD.What reads price_scale: CryptopoolLPOracle prices the LP from virtual_price, price_scale, and the crvUSD aggregator, and the LEVAMM uses that price, p_o, for x0, quotes, safe-band checks, and value_oracle(). LT prices a share from value_oracle() and price_scale times the crvUSD price, which gives pricePerShare(), the Fundamental Value. preview_withdraw() sizes the share of LEVAMM collateral and debt with oracle values, then prices the exit from live pool balances, which gives the Redemption Value. The gap between the two is TRD.

The two prices that read price_scale work in this way:

  • The oracle computes price() from virtual_price, price_scale, and the aggregator. The LEVAMM uses this price, pop_o, for x0, its quotes (get_p(), get_dy()), its safe-band checks, and value_oracle(). State-changing calls, such as exchange and _deposit, read it with price_w().
  • LT prices the asset at price_scale times the aggregator price. _calculate_values combines this price with value_oracle() to give pricePerShare(), the Fundamental Value.
  • preview_withdraw() sizes the share of the LEVAMM collateral and debt with the same oracle values. It then prices the exit with calc_withdraw_fixed_out on the live pool balances, not at pop_o. The result is the Redemption Value, and the gap between the two values is TRD.
  • When price_scale moves, get_p() of the LEVAMM moves with pop_o. A gap between get_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_scale changes only when the pool rebalances. Only the first operation in a block that calls tweak_price can 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 as AMM.exchange() and AMM._deposit(), call price_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 (0.90,1.10)(0.90, 1.10), 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_scale by at most the pool's own step limit (adjustment_step_max).
  • Only the first operation in a block that calls tweak_price can 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
  • 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.