Skip to main content

Oracle design

The YieldBasis LP oracle, CryptopoolLPOracle.vy, returns the USD value of one Curve liquidity provider (LP) token. Each call reads live state: two values from the Curve Cryptoswap pool, virtual_price and price_scale, and the crvUSD price from Curve's price aggregator. Because every call reads live state, the oracle needs no update schedule and no staleness check.

The oracle reads neither an external price of the asset nor the pool spot price. A flash loan can push a spot price far from the market for one block. It can then fool an oracle that reads the spot price.

Key terms​

These terms appear throughout this page:

  • price_scale is the pool's internal price. The pool concentrates liquidity around it and moves it in limited steps toward a target.
  • virtual_price is the pool's growth accounting per LP token.
  • An exponential moving average (EMA) gives more weight to recent observations. So one new price changes a long EMA only a little.
  • price_oracle is the pool's own EMA of trade prices.

For other terms, read Glossary.

LP price formula​

The oracle computes the LP token price, in USD, with this formula:

lp_price=2⋅vp⋅price_scale⋅pagg\mathrm{lp\_price} = 2 \cdot v_p \cdot \sqrt{\mathrm{price\_scale}} \cdot p_{\text{agg}}

The formula has these inputs:

  • vpv_p: the pool's virtual_price.
  • price_scale\mathrm{price\_scale}: the pool's internal price of the volatile asset in crvUSD.
  • paggp_{\text{agg}}: the crvUSD/USD price from Curve's price aggregator.

CryptopoolLPOracle.vy implements this formula (lp_price() and price()). The contract has no EMA and no stored state. The only EMA on this path is inside the crvUSD aggregator. LEVAMM, the market's leveraged automated market maker, reads this oracle as its price input pop_o (AMM.PRICE_ORACLE_CONTRACT).

The following example shows the effect of a lag in price_scale. BTC trades at $100,000, the BTC price of the canonical position. Suppose price_scale lags at $90,000, 10% below the market, and virtual_price stays the same. The formula takes the square root of price_scale, and √0.9 ≈ 0.949. So the oracle values the LP token about 5.1% below its value at a price_scale of $100,000.

Factor-by-factor meaning​

Each part of the formula has one role:

  • 2⋅vp⋅price_scale2 \cdot v_p \cdot \sqrt{\mathrm{price\_scale}} is the value of one LP token in crvUSD, on the assumption that the pool is in balance at price_scale.
  • paggp_{\text{agg}} converts crvUSD to USD. The aggregator is an external Curve contract. YieldBasis does not control it. A swap in one YieldBasis pool does not move it far.

Price path to the LP oracle​

The following figure shows how a market price reaches the LP oracle and LEVAMM:

Flowchart of the price path to the LP oracle. Swaps move the Curve pool spot price, and the pool averages trade prices into price_oracle. price_oracle is the price_scale target when no policy is attached or the policy is killed. An attached pool policy sets the target instead, from pool prices and, on the oracle-report policy only, signed Pyth Pro price reports. The target stays within 20% of price_oracle, and price_scale moves toward it in limited steps. The LP oracle computes 2 × virtual_price × √price_scale × crvUSD/USD, and LEVAMM uses that price for its quote and leverage.Flowchart of the price path to the LP oracle. Swaps move the Curve pool spot price, and the pool averages trade prices into price_oracle. price_oracle is the price_scale target when no policy is attached or the policy is killed. An attached pool policy sets the target instead, from pool prices and, on the oracle-report policy only, signed Pyth Pro price reports. The target stays within 20% of price_oracle, and price_scale moves toward it in limited steps. The LP oracle computes 2 × virtual_price × √price_scale × crvUSD/USD, and LEVAMM uses that price for its quote and leverage.

Traders swap in the Curve pool, so the pool spot price follows the market price. The pool averages its trade prices into price_oracle. Without a pool policy, or after a policy is killed, price_oracle is the price_scale target. If a pool policy is attached, it sets the target from the pool's own prices. The oracle-report policy also uses signed Pyth Pro price reports that traders deliver. The pool keeps the target within 20% of price_oracle, and price_scale moves toward it in limited steps. The LP oracle reads price_scale, virtual_price, and the crvUSD price, and LEVAMM uses the result as its price input.

The pool spot price and external prices never enter the LP oracle directly. They reach it only through price_scale. On a pool that runs the oracle-report policy, a signed price report reaches the LP oracle only by moving the price_scale target.

Step limits on price_scale​

price_scale moves in limited steps toward its target. The rules depend on the pool version.

The v3 pools of markets 7 to 10 follow these rules:

  • Only the first pool operation in a block can change price_scale, and only if that operation passes the pool's profit test, vp_boosted > lp_xcp_profit.
  • The move must also keep the LP profit floor, lp_xcp_profit.
  • Each step is at most adjustment_step_max. The pool does not move when the step is at or below adjustment_step_min. To check both, call adjustment_step() on the pool: it returns [min, max].

The older pools of markets 0 to 6 run Twocrypto v2.1.0d. They follow these rules:

  • They also change price_scale at most once per block, toward their own price_oracle.
  • They keep half of the profit growth for LPs as the floor.
  • On the BTC pools of markets 0 to 5, each step is at least one fifth of the gap to price_oracle, with no upper limit.
  • On the WETH pool of market 6, each step is at most adjustment_step().

On all pools, a sandwich attack inside one block cannot move price_scale by more than one step.

price_scale is not an EMA. It follows its target over hours. The lag between price_scale and the market price is one cause of the Temporary Redemption Discount (TRD). For how TRD forms and closes, read Fundamental Value, Redemption Value, and TRD.

What sets the price_scale target​

The pool moves price_scale toward a target. On v3 pools, a pool policy can set that target. A pool policy is a contract that a Curve Cryptoswap pool calls to set its swap fee, its price_scale target, or both. To see which policy a pool runs, call POLICY() on the pool or read the table in Contract addresses.

On a pool that runs the oracle-report policy, a price report is a signed USD price of the asset. A report steers the target while it is younger than the report lifetime, a policy setting. The table shows the target for each policy type:

PoolTargetExternal price
Pool without a policy hook (older code), or with no policy attachedThe pool's own EMA of trade prices, price_oracleNone
Pool that runs YBTwocryptoPolicyExtrapolated from two EMAs of the pool's own trade pricesNone
Pool that runs the oracle-report policy, YBOraclizedPolicyTwo EMAs of the price report during the report lifetime, else of the pool's spot priceSigned Pyth Pro (Pyth Lazer) USD price reports

For every policy, the pool clamps the target to within 20% of price_oracle. The step limit and the profit floor still apply.

External price input through the pool policy​

On a pool that runs the oracle-report policy, YBOraclizedPolicy, a trader delivers price reports to the policy with deliver_oracle_report. Pyth Pro signs each report. Signed price reports steer the price_scale target within step and clamp limits. If the YieldBasis DAO (YB DAO) or the Curve Emergency DAO kills the policy, the pool uses its native target, price_oracle.

The LP oracle formula does not change. The external price enters the LP valuation only through price_scale.

The policy also sets the swap fee from the report, but the fee does not enter the LP oracle. The policy counts 1 crvUSD as $1, and the LP oracle still multiplies by the crvUSD aggregator price.

For how the policy builds the target from its two EMAs, read price_scale target in Pool policies.

Derivation of the balanced-pool form​

At balance, the pool holds equal USD value on each side. Let reserves be xx (crvUSD) and yy (the volatile asset), with xy=kpoolxy = k_{\text{pool}}. At balance x=kpool⋅price_scalex = \sqrt{k_{\text{pool}} \cdot \mathrm{price\_scale}}, so the total pool value is 2kpool⋅price_scale2\sqrt{k_{\text{pool}} \cdot \mathrm{price\_scale}}. Divide by the LP supply and express kpoolk_{\text{pool}} in virtual-price terms. The result is vp⋅2⋅price_scalev_p \cdot 2 \cdot \sqrt{\mathrm{price\_scale}} per LP token.

Safety band on the crvUSD aggregator​

The Factory is the contract that deploys each market. The Factory checks that the crvUSD aggregator price is within (0.90,1.10)(0.90, 1.10), open on both ends. It does this check when the deployer sets the aggregator and when governance changes it with set_agg. Outside the band, the setter reverts and the previous aggregator stays in use.

Each market's LP oracle stores its aggregator address when the Factory creates the market. A later set_agg call changes the aggregator only for markets created after it. Deposits, withdrawals, and exchanges do not check the band again.

Lending oracles​

LEVAMM reads CryptopoolLPOracle.vy directly, and a separate set of contracts prices yb-LP shares for lending markets. For how those contracts price a share and where to find their addresses, read Lending oracles.