Skip to main content

How oracle-report swaps are priced

On a Curve pool that runs the oracle-report policy (YBOraclizedPolicy), the policy sets the fee of each swap from a signed price report. A price report is a signed Pyth Pro (Pyth Lazer) USD price of the pool's asset. The fallback fee is the fee without a discount. While the policy is not killed, only a corrective swap with a positive edge and a price report younger than the relax time (Fee settings) can pay less than the fallback fee. The discount fades with price report age.

To deliver a price report and swap in one transaction, follow Oracle report arbitrage. For why a pool runs this policy, read Why a pool runs the oracle-report policy in Pool policies.

How the fee works​

A pool policy is a contract that the Curve pool asks for its swap fee and its price_scale target. price_scale is the pool's internal price. For every fee, the Curve pool calls policy.get_fee(xp). xp holds the pool balances after the operation and before its fee. In xp, the asset is scaled by price_scale.

Fees here are in basis points (bp): 1 bp is 0.01%. The pool clamps a non-zero result to the range 0.1 bp to 100%. A result of 0 tells the pool to use its native fee. For the pool side of this call, read Pool policy.

Fee settings​

The age of a price report is the block time minus its observation time. The policy reads four fee settings from policy.config().fee. Read them before you trade. The owner can change them within the limits in Pool policy. The examples on this page use the values in the Example column:

FieldMeaningUnits onchainExample
base_feeLowest fee of a discounted swap1e10 = 100%50,000,000 (50 bp)
captureShare of the edge above base_fee that the pool keeps1e18 = 100%0.675e18 (67.5%)
fallback_feeFee without a discount, and the cap1e10 = 100%175,000,000 (175 bp)
relax_timePrice report age at which the discount is goneseconds300

Onchain fees count in fee units. On the 1e10 = 100% scale, one fee unit is 1e-10.

Snapshot and corrective swap​

The policy compares each operation with its snapshot. The snapshot is the pool state that the policy stored at the last pool callback or sync(). In normal operation, it equals the pool state before your swap.

A corrective swap moves the pool's marginal price toward the price report and does not pass it. The policy computes the marginal price with cached_A, the pool's A that the policy caches at deployment and at each sync(). If either balance becomes 0, the swap is not corrective. With p_before and p_after as the marginal price before and after the swap, the test for each direction is:

  • coin 0 (crvUSD) in: p_before < p_after and p_after ≤ report_price.
  • coin 1 (the asset) in: p_before > p_after and p_after ≥ report_price.

Edge​

The edge is the gain of the whole swap at the price report, before the fee. The policy values both legs in crvUSD and counts 1 crvUSD as $1. These formulas give the edge, first in general and then for each direction:

edge = 1 − input_value / output_value      (0 if input_value ≥ output_value)

buy asset (crvUSD in): edge = 1 − crvUSD_in / (asset_out × report_price)
sell asset (asset in): edge = 1 − (asset_in × report_price) / crvUSD_out

output_value is the output before the fee. The code rounds the edge up to the next fee unit. The edge equals the fee at which the swap breaks even at the price report.

Check order​

_get_fee, the policy's internal fee function, runs these checks in this order. The earliest check that fails sets the result:

  1. If the policy is killed, the result is 0. The pool then charges its native fee.
  2. If both balances in xp are equal to or more than the snapshot balances, the result is fallback_fee.
  3. If both balances are equal to or less than the snapshot balances, the result is fallback_fee.
  4. If the policy holds no price report (asset_price is 0), the result is fallback_fee.
  5. If the price report age is relax_time or more, the result is fallback_fee.
  6. If the snapshot D is 0 or differs from the pool's stored D, the result is fallback_fee.
  7. If the edge of the swap at the price report is 0, the result is fallback_fee.
  8. The policy computes the discounted fee.
  9. If that fee is less than fallback_fee and the swap is corrective, the result is the discounted fee.
  10. Otherwise, the result is fallback_fee.

Check 6 fails after a failed best-effort removal callback. It also usually fails during an A ramp. For the same checks in the policy source, read Pool policy.

Only a swap moves one balance up and the other down, so checks 2 and 3 give liquidity operations fallback_fee. A yb-LP deposit or withdrawal pays half the pool fee rate on the imbalanced part. preview_deposit and preview_withdraw include this fee. For the rule, the spam penalty, and a one-coin example, read get_fee in Pool policy.

Fee formula​

After the checks, the policy computes the fee in two parts: a fresh fee for a price report of age 0, and an increase with age. This formula gives the fee:

excess = max(edge − base_fee, 0)
fresh = min(fallback_fee, base_fee + capture × excess)

age = 0: fee = fresh
0 < age < relax_time: fee = fresh + isqrt((fallback_fee − fresh)² × age / relax_time) + 1 fee unit
(if fresh = fallback_fee, fee = fallback_fee)
age ≥ relax_time: fee = fallback_fee

The second line equals fresh + (fallback_fee − fresh) × √(age / relax_time), plus one fee unit (1e-10). The age is block.timestamp minus the observation time of the price report, in microseconds. If the observation time is after block.timestamp, the age is 0.

The fee is never above fallback_fee. So a price report can only lower the fee of a swap. For the source form of the edge and fee formulas, read Pool policy.

Profit at a fresh price report​

Below the cap and at a price report age of 0, this formula gives the profit:

profit = V_out × (edge − fee) = V_out × (1 − capture) × (edge − base_fee)

V_out is the output value at the price report, before the fee. With the example settings, you keep 32.5% of the edge above 50 bp. The pool keeps 67.5% of it, plus the 50 bp base_fee. This table gives the fee and your share at each edge:

EdgeFee (age 0)You keep, before gas and hedge
0 bpfallback_fee (check 7)none
50 bp50.00 bp0.00 bp
100 bp83.75 bp16.25 bp
150 bp117.50 bp32.50 bp
200 bp151.25 bp48.75 bp
235 bp174.88 bp60.13 bp
300 bp175.00 bp125.00 bp

The discounted fee reaches fallback_fee at edge = base_fee + (fallback_fee − base_fee) / capture. With the example settings, this is 50 + 125 / 0.675 ≈ 235.2 bp. At a larger edge, the price report does not change the fee. The swap is an ordinary 175 bp trade.

At the price report, a swap with an edge of exactly 50 bp breaks even. A swap with a smaller edge loses money. Trade only when the pool price is more than base_fee from the price report. For how far to trade, read Size of the trade in Oracle report arbitrage.

Price report age​

The discount fades with the square root of the age. This table shows the fee on a 120 bp edge (fresh fee 97.25 bp) at each age:

AgeWeight on fallback_feeFeeYou keep
0 s0%97.25 bp22.75 bp
1 s5.8%101.74 bp18.26 bp
3 s10.0%105.03 bp14.97 bp
12 s20.0%112.80 bp7.20 bp
60 s44.7%132.02 bp−12.02 bp
300 s100%175.00 bp−55.00 bp

One block of delay (12 s) removes 20% of the discount. So deliver the price report and swap in the same transaction, as Oracle report arbitrage shows.

How a price report moves price_scale​

Signed price reports also steer the price_scale target within step and clamp limits. The target affects when the Curve pool rebalances. Through price_scale, the target also moves the oracle price of the LEVAMM, the market's leveraged AMM.

The policy keeps a fast and a slow exponential moving average (EMA) of an observed price. The driver settings are in policy.config().driver. This table gives the meaning of each setting, with example values:

SettingExampleMeaning
fast_half_life4,800 s (80 min)Half-life of the fast EMA
slow_half_life9,600 s (160 min)Half-life of the slow EMA
trend_gate0.007e18 (0.7%)Trend (the relative gap between the two EMAs) at which the target equals the observed price
step_cap0.005e18 (0.5%)Largest requested move per touch, as a share of price_scale
report_lifetime5,400 sAge below which the EMAs observe the price report

A price report affects arbitrage in these ways:

  • A delivered price report can change the price_scale target as soon as the policy stores it.
  • A swap in the same transaction can trigger a step. The swap must be the first operation in the block that calls tweak_price. It must also pass the pool's profit test (vp_boosted > lp_xcp_profit).
  • A price_scale move changes the LEVAMM oracle price 2 × virtual_price × √price_scale × crvUSD price. That can open a VirtualPool arbitrage against the LEVAMM.
  • A price_scale move also moves the pool's liquidity concentration. Read the pool state again after each block.

For the observation, target, and move formulas, read price_scale driver in Pool policy. For every pool limit on the step, read get_price_scale in Pool policy. For the step rules of each pool version, read Step limits on price_scale in Oracle design.

  • Oracle report arbitrage: deliver a price report and swap in one transaction, and size the trade.
  • Pool policy: the policy source behind the fee checks, the price_scale driver formulas, and the owner limits.
  • Pool policies: the concept and the LP view of both pool policies.
  • Arbitrage: the LEVAMM trade through the VirtualPool, which a price_scale move can open.