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:
| Field | Meaning | Units onchain | Example |
|---|---|---|---|
base_fee | Lowest fee of a discounted swap | 1e10 = 100% | 50,000,000 (50 bp) |
capture | Share of the edge above base_fee that the pool keeps | 1e18 = 100% | 0.675e18 (67.5%) |
fallback_fee | Fee without a discount, and the cap | 1e10 = 100% | 175,000,000 (175 bp) |
relax_time | Price report age at which the discount is gone | seconds | 300 |
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_afterandp_after ≤ report_price. - coin 1 (the asset) in:
p_before > p_afterandp_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:
- If the policy is killed, the result is 0. The pool then charges its native fee.
- If both balances in
xpare equal to or more than the snapshot balances, the result isfallback_fee. - If both balances are equal to or less than the snapshot balances, the result is
fallback_fee. - If the policy holds no price report (
asset_priceis 0), the result isfallback_fee. - If the price report age is
relax_timeor more, the result isfallback_fee. - If the snapshot
Dis 0 or differs from the pool's storedD, the result isfallback_fee. - If the edge of the swap at the price report is 0, the result is
fallback_fee. - The policy computes the discounted fee.
- If that fee is less than
fallback_feeand the swap is corrective, the result is the discounted fee. - 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:
| Edge | Fee (age 0) | You keep, before gas and hedge |
|---|---|---|
| 0 bp | fallback_fee (check 7) | none |
| 50 bp | 50.00 bp | 0.00 bp |
| 100 bp | 83.75 bp | 16.25 bp |
| 150 bp | 117.50 bp | 32.50 bp |
| 200 bp | 151.25 bp | 48.75 bp |
| 235 bp | 174.88 bp | 60.13 bp |
| 300 bp | 175.00 bp | 125.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:
| Age | Weight on fallback_fee | Fee | You keep |
|---|---|---|---|
| 0 s | 0% | 97.25 bp | 22.75 bp |
| 1 s | 5.8% | 101.74 bp | 18.26 bp |
| 3 s | 10.0% | 105.03 bp | 14.97 bp |
| 12 s | 20.0% | 112.80 bp | 7.20 bp |
| 60 s | 44.7% | 132.02 bp | −12.02 bp |
| 300 s | 100% | 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:
| Setting | Example | Meaning |
|---|---|---|
fast_half_life | 4,800 s (80 min) | Half-life of the fast EMA |
slow_half_life | 9,600 s (160 min) | Half-life of the slow EMA |
trend_gate | 0.007e18 (0.7%) | Trend (the relative gap between the two EMAs) at which the target equals the observed price |
step_cap | 0.005e18 (0.5%) | Largest requested move per touch, as a share of price_scale |
report_lifetime | 5,400 s | Age below which the EMAs observe the price report |
A price report affects arbitrage in these ways:
- A delivered price report can change the
price_scaletarget 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_scalemove changes the LEVAMM oracle price2 × virtual_price × √price_scale × crvUSD price. That can open a VirtualPool arbitrage against the LEVAMM. - A
price_scalemove 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.
Related
- 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_scaledriver 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_scalemove can open.