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_scaleis the pool's internal price. The pool concentrates liquidity around it and moves it in limited steps toward a target.virtual_priceis 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_oracleis 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:
The formula has these inputs:
- : the pool's
virtual_price. - : the pool's internal price of the volatile asset in crvUSD.
- : 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 (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:
- is the value of one LP token in crvUSD, on the assumption that the pool is in balance at
price_scale. - 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:
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 belowadjustment_step_min. To check both, calladjustment_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_scaleat most once per block, toward their ownprice_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:
| Pool | Target | External price |
|---|---|---|
| Pool without a policy hook (older code), or with no policy attached | The pool's own EMA of trade prices, price_oracle | None |
Pool that runs YBTwocryptoPolicy | Extrapolated from two EMAs of the pool's own trade prices | None |
Pool that runs the oracle-report policy, YBOraclizedPolicy | Two EMAs of the price report during the report lifetime, else of the pool's spot price | Signed 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 (crvUSD) and (the volatile asset), with . At balance , so the total pool value is . Divide by the LP supply and express in virtual-price terms. The result is 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 , 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.
Related
- Pool policies: what each policy sets and who controls it.
- Compounding leverage: how LEVAMM uses this oracle.
- Dev: CryptopoolLPOracle: the contract that implements the formula on this page, with its getters.
- Dev: Pool policy: the deployed policy settings, the price report admission rules, and the target formulas.