Liquidity shaping
A pool’s depth curve maps how much of a leg a trade consumes onto the price offset that trade pays. The on-chain object is a clamped quartic I-spline: monotone by construction, C2 at every knot, and integrable in constant time whatever the trade size. This page covers how a curve is stored and validated, how the shipped presets were fitted, how live dispersion scales one, and how a swap traverses it.
1. What the curve is
The curve is a function
mapping cumulative depth (in bps of the leg’s reserves, so ) to a relative price offset (in pbps, stored at fixed point). It is not a price. It is not denominated in any token. A quote is formed only when the offset is applied to the oracle mark:
with the oracle mark and the offset after dispersion scaling (§6.1).
Four properties follow:
- The curve is anchor-free. is a fraction of depth in the profile asset’s own token units (§7), is a dimensionless relative offset. Nothing in the curve knows which asset a leg is anchored to. Only the mark carries denomination, and under the multi-anchor design the mark is attested in parent units. See Anchor Path Pricing.
- The curve is monotone by construction. Control weights are validated nondecreasing
(
NUQuartic.sol), which for a B-spline is equivalent to a nondecreasing integral at any degree. A non-monotone segment would mean negative marginal liquidity and cannot be expressed. This is condition C2 of the cycle-safety proof: without it, a closed trading cycle through the tree could return more than it consumed. - The density is C2. Degree 4 with simple interior knots gives two continuous derivatives at every knot. Marginal liquidity therefore varies smoothly, so there is no density step a taker can size a trade against, and monotonicity is a linear constraint on the control weights rather than a per-segment tangent condition (§3.1).
- The curve is shared, not per-asset. Curves live in a per-pool preset table and assets point at one (§2.2). A refit updates every asset that references the preset.
2. Storage
2.1. Layout
struct Curve {
uint256 header; // m(uint8) | b1..b13(13 x uint16) | median(uint16) | dispRefPbps(uint16) | flags(uint8)
uint256[28] segs; // segs[2i] = c0|c1|c2|c3 (4 x int64); segs[2i+1] = c4(int64) | S(int128)
}NUQuartic.sol. One header slot plus two slots per segment, at most 14 segments
(MAX_SEGS, NUQuartic.sol: 13 interior boundaries in the header plus the constant right edge).
| Field | Bits | Meaning |
|---|---|---|
m | header[0:8] | Segment count, 1 to 14 |
b_1..b_13 | header[8:216] | Interior right boundaries in x, uint16 each |
median | header[216:232] | Density median in bps, the curve’s own zero (§5.1) |
dispRefPbps | header[232:248] | Reference dispersion in pbps that the fit was built at |
flags | header[248:256] | bit 0 = FLAG_REQUIRES_WALL |
c_0..c_4 | segs[2i], segs[2i+1] low | Power-basis coefficients on local , int64, pbps |
segs[2i+1] high | Exact prefix integral , int128, pbps |
The directory holds the interior boundaries only. The last, , is always
: a constant a 14-segment curve would store 14 times. _frame takes that edge from the
constant instead, and the freed uint16 carries median at zero extra storage, zero extra
SLOADs and an unchanged MAX_SEGS. The header still resolves the full directory, so _frame locates
a segment with pure word operations.
Header fields are read through named accessors (NUQuartic.median, dispRefOf, requiresWall), not
by hand-written shifts.
2.2. Preset table and asset pointer
Canonical for the codebook rationale. Parametrization §9.2 and the Overview summarise it only.
Curves live in a shared per-pool table PoolStorage.curves, mapping(uint16 => NUQuartic.Curve). Each
asset carries Asset.presetId (uint16). presetId = 0 is the explicit no-shape sentinel and is
refused at config: PoolConfig.validatePresetAssign will not list an asset without a curve, so
every leg that can be quoted carries one and there is no shapeless code path (§9).
An asset does not own a curve. It points at one and scales it by
. The reference roster carries 5 presets across 28 legs; the live Arc fleet ships four of them (preset 3 unused)
across 26 symbols (reference-roster values; per-parameter tables in Parametrization). A stable and a volatile
share a preset id when they want the same width; the five presets carry three distinct
wQ vectors and are not interchangeable rescalings of one another (§4.3).
The table is a quantized density codebook. An asset’s depth density is fitted off chain from its observed tape and then mapped to the nearest entry already in the table, rather than written to chain as its own curve. Two costs drive that design, an order of magnitude apart:
- Writing a curve is expensive and rare. It runs the full validation and segment build, costs the
gas in §4.5, and is timelocked (
requestOp(..., UPDATE_CURVE, ...)/executeSetCurve, §10). - Re-pointing an asset is cheap and frequent.
Asset.presetIdis a uint16 pointer, so as an asset’s observed density drifts the keeper moves it to a different codebook entry throughsetProfilewithout touching any curve.
The price of quantization is the residual between an asset’s own fit and its nearest codebook entry. The continuous y-scale (§6.1) absorbs the scale part of that residual exactly, at any value, with no refit. The codebook therefore has to span only shape: how the density is distributed across the depth axis, not how wide it is. Five entries cover 28 legs because those legs disagree about width far more than about shape.
2.3. Validation at install
NUQuartic._validate (NUQuartic.sol) and NUQuartic.set (NUQuartic.sol):
| Property | Requirement |
|---|---|
dispRefPbps | (it is a divisor in _scaleY; zero would brick every quote) |
wQ length | , |
interior length | exactly |
| Non-flat | wQ[n-1] != wQ[0] |
| Monotone | wQ[i] >= wQ[i-1] for all |
| Interior knots | strictly increasing, in |
| Coefficients | $ |
Segment count is . The knot vector is clamped degree 4: five copies of 0, the interior knots, five copies of BPS.
The stored curve is centered on the mark. Before building the segments, set shifts the whole
polygon by so that , i.e. (NUQuartic._centre, NUQuartic.sol). Clamped endpoints make
and , so the transform is one subtraction per weight. It is
shape-preserving (the fitted density is untouched), idempotent, and exact but for the 1
Q-unit an odd sum cannot split, and a no-op on all five shipped presets, whose closed-form fits
are already antisymmetric.
A level bias between the mark and the curve is unpriced
optionality, and admitting one forced the interior leg to re-center a mid the terminal leg did not,
which put two prices on one tree edge (Anchor Path Pricing §3.1).
Curves are centered rather than refused because a fitted control polygon lands within about
0.5 pbps of antisymmetric, so an equality check would reject every real curve over its own fitting
residual. Centering subsumes any admission bound on : magnitudes only shrink under the shift,
so the int64 coefficient bound still gates the shape.
The density median is computed and stored at the same write. Centering pins ,
so is the integral of a nonnegative density running from to and
the single with is the point that splits the density in half. NUQuartic.set
writes the segments first, then binary-searches the stored power-basis curve through the same
evalQ the quote path runs (14 evaluations, bracketed by ), and packs
the result into the header. Storing it rather than root-finding at the read is a hot-path decision:
the swap path may not pay 14 evaluations. Because is integer bps, is bounded by one
x-unit of density rather than by zero; on an antisymmetric curve it is exactly zero and
. This is the value Pricing._skewToDepth anchors zero inventory skew on (§5.1).
At assignment time PoolConfig.validatePresetAssign (PoolConfig.sol) adds two checks:
- The preset must carry a real curve:
header == 0revertsNotConfigured(ASSET, token).presetId = 0is the empty sentinel andsetCurverefuses to install a shape there, so a curveless asset is unconstructible. - A preset with
FLAG_REQUIRES_WALLmay only be assigned to an asset withkappaCovBps != 0(PoolConfig.sol, revertsBadConfig).
The price multiplier’s positivity needs no check of its own: sanitizeDispersion holds the asset’s
minDispersionPbps under Pricing.dispersionCap, which is INTERIOR_SWING_CAP_PBPS expressed in the
preset’s own units, so the deepest offset the shape can quote at that floor is half the swing cap,
5000 against PBPS’s 1e6. MAX_DISPERSION_PBPS = 900000 (90% of PBPS) is the separate hard bound
(PoolConstantsLib.sol, clamped in Pricing._calculateDispersion).
3. Spline mathematics
3.1. Why an I-spline
An I-spline is the integral of an M-spline, a nonnegative B-spline basis. Writing
with clamped quartic B-splines , monotonicity of is equivalent to . That is a linear constraint on the coefficients, checkable exactly on-chain in integer comparisons. No tangent clamps, no per-segment monotonicity proofs, no floating point.
3.2. Conversion to power basis
At install, _segCoeffs (NUQuartic.sol) converts each span to power basis on local
:
come from the left jet of the span (value, first and second derivative from de Boor), and from the right-end value and slope:
with . (NUQuartic.sol) is an extra fixed-point scale carried through
the derivative pyramids: slope truncation feeds , so a 1-unit error in would
cost units of , which is at a 5000-unit span. caps that residual
at about 13 units of pbps, i.e. pbps.
Value, slope and are continuous across spans, so evaluating the left jet inside a span equals the previous span’s right jet. No carry is needed between spans.
3.3. Evaluation
NUQuartic.evalQ (NUQuartic.sol): locate the segment from the header, load two slots, Horner
on five coefficients.
3 cold SLOADs total (header plus two segment slots). is clamped to by
if (dx > h) dx = h.
3.4. Exact O(1) integration
VWAP needs . Each segment stores the exact prefix integral to its left edge, so
NUQuartic.areaQ (NUQuartic.sol) returns 0 when , otherwise differences two
_at calls. _at (NUQuartic.sol) is the stored prefix plus the local quintic primitive:
Result units are pbps. 5 cold SLOADs (header plus two slots for each of the two boundary segments), and the cost is independent of how many segments the trade crosses: the prefix integrals turn what would be a per-segment sum into one subtraction.
The full-segment integral stored at install is exact in closed form (NUQuartic.sol):
4. Preset: central-normal plateau
The shipped shape. Every codebook entry is a central-normal plateau.
4.1. Closed form
The signed quote offset is modelled as truncated at the empirical q70, the exact 30% folded-tail cut. The depth axis is the CDF of that distribution, so the offset curve is its quantile function:
is the half-swing: half the peak-to-peak offset range, evaluated at the fit’s reference
dispersion dispRefPbps. Away from the knots the shipped spline reproduces the closed form to better
than 0.005 pbps at the fitted scale; at a knot it is pinned to the control weight instead and the
deviation is larger:
| (bps) | Closed form, | Shipped spline (preset 1/2) |
|---|---|---|
| 0 | ||
| 1314 | ||
| 2500 | ||
| 5000 | ||
| 7500 | ||
| 10000 |
(The 1314 row is the knot itself, where the spline is pinned to the control weight rather than to the target: 0.175 pbps, or 0.175% of .)
The density is , whose reciprocal is minimized at . Density is therefore maximal at the mark and falls off symmetrically to both edges. Because is flat near 0, the top is a genuine table-top: not a dome, not a spike. Measured local slopes on the shipped preset (): 0.0246 pbps/bp on , 0.0202 on , 0.0185 on , 0.0172 on .
4.2. Structure
segments, two interior knots at bracketing the mark at . The middle segment carries the flat top, the two outer segments carry the shoulder roll-off. storage slots.
The model behind all five entries is the one closed form of §4.1 evaluated at three half-swings, and the ratio is what the live dispersion then scales continuously (§6). That is a statement about the model, not about the bytes on chain: each vector was fitted and rounded independently, so the stored weights are not exact multiples of one another. §4.3 gives the residual and what it is worth at a quote.
4.3. Shipped presets (5 rows)
Control weights are scaled: wQ[0] = -H_pbps * 1e9, wQ[6] = +H_pbps * 1e9.
| ID | wQ vector | dispRefPbps (pbps) | Wall-gated | Seeded from | ||
|---|---|---|---|---|---|---|
| 1 | 1 bp = 100 pbps | A | 100 | 1 | No | USDC |
| 2 | 1 bp = 100 pbps | A | 100 | 1 | Yes | USDT |
| 3 | 2 bp = 200 pbps | B | 100 | 2 | Yes | USDE |
| 4 | 5 bp = 500 pbps | C | 100 | 5 | Yes | USDG |
| 5 | 5 bp = 500 pbps | C | 500 | 1 | No | WETH |
All five share interiorB = [1314, 8686] and a 7-element weight vector. The three distinct vectors,
in pbps:
A = [-100000000000, -90053719349, -38039704575, 0, 38039704575, 90053719349, 100000000000]
B = [-200000000000, -180107438698, -76079409151, 0, 76079409151, 180107438698, 200000000000]
C = [-500000000000, -450268596744, -190198522877, 0, 190198522877, 450268596744, 500000000000]Three distinct wQ vectors, not one shape scaled five ways. Presets 1 and 2 are byte-identical in
shape and differ only in FLAG_REQUIRES_WALL; presets 4 and 5 are byte-identical in shape and differ
only in dispRefPbps (100 against 500). The three vectors that remain are not exact multiples of each
other, because each is an independently-rounded fit of the §4.1 closed form:
| Relation | Exact multiple of A | Stored | Residual |
|---|---|---|---|
A[2] against B[2] | 1 ULP | ||
A[1] against C[1] | 1 ULP | ||
A[2] against C[2] | 2 ULP |
One ULP is pbps at the fitted scale. _scaleY multiplies by before
truncating to whole pbps, and that amplification is bounded by dispersionCap at 10x (§6.5), so the
residual reaches at most pbps against a 1 pbps quote granularity: it does not on
its own move a quoted mid.
It matters for a different reason. The shipped vectors are canonical on-chain state, so a vector regenerated by rescaling another will not reproduce the shipped bytes and will fail parity against on-chain state and against the research parity vectors. Treat the presets as five independent curves:
- Do not derive one from another.
- Do not assume a quote on preset 3 equals twice a quote on preset 1.
- Re-fit and compare against the shipped values rather than rescale when refreshing an entry.
The ratio is what sets a preset’s width at quote time. The live half-swing of the book is
so with dispersion in pbps, presets 1, 2 and 5 reach exactly pbps at the edge, preset 3 reaches , preset 4 reaches .
4.4. Quiet-tape half-swings
Using deployed minDispersionPbps. These are the quiet-tape () widths; under the
adaptive-dispersion law every leg quotes above its floor whenever , one-for-one at
shipped vega (§6.4):
| Asset | Preset | minDispersionPbps | Quiet-tape edge | |
|---|---|---|---|---|
| USDC | 1 | 1 | 200 | 200 pbps = 2.00 bp |
| USDT | 2 | 1 | 161 | 161 pbps = 1.61 bp |
| USDG | 4 | 5 | 73 | 365 pbps = 3.65 bp |
| DAI | 3 | 2 | 559 | 1118 pbps = 11.18 bp |
| USDTB | 2 | 1 | 470 | 470 pbps = 4.70 bp |
| WETH | 5 | 1 | 2034 | 2034 pbps = 20.34 bp |
| XAUT | 5 | 1 | 1500 | 1500 pbps = 15.00 bp (was 3601; cut 2026-09-04 with the metal class) |
| KRW1 | 5 | 1 | 3740 | 3740 pbps = 37.40 bp |
4.5. Gas
Harness: NUQuarticSetGas.t.sol, measuring cold and warm NUQuartic.set by segment count and
by shipped preset. The figures below are recorded run values, not assertions in the benchmark, so
re-measure before quoting them externally.
| Shape | Slots | setCurve update (warm) | First set (cold) | |
|---|---|---|---|---|
| Central-normal plateau (shipped) | 3 | 7 | ~289,800 | ~409,500 |
Cost is linear in segment count: two storage slots per segment plus the header, written once and read three or five times per quote (§11.1). At every codebook entry sits at the cheapest end of that scale, making a write-once, point-often codebook (§2.2) cheap to maintain.
5. Inventory skew mapping (skew -> depth)
For how is computed from coverage, see Inventory Management §3.
5.1. The Center
Pricing._skewToDepth:
is the curve’s own density median, read from the header (§2.1, §2.3). It is the one at which the curve quotes the mark itself, so zero inventory skew quotes the mark for every shape.
| (antisymmetric preset, ) | Meaning | |
|---|---|---|
| 0 | Maximum discount, pool is over-covered and wants to sell | |
| = 5000 | Center, quote at the mark | |
| 10000 | Maximum premium, pool is under-covered and wants to buy |
One definition of center, held on both sides of the write. _centre pins
at the write, which puts the density median on the mark, and
_skewToDepth starts the traverse from that same median at the read. The two agree by
construction on any shape. The domain midpoint is not the anchor and only coincides
with it on an antisymmetric : all five shipped presets are antisymmetric on symmetric knots, so exactly on each, but anchoring on the midpoint would quote
at balanced coverage on any asymmetric shape, up to half the peak-to-peak swing, and no read path
would correct it. On a shape pinned at the widest dispersion its own fence admits that is 42 bps
off mark at zero inventory skew, 4.2 bps at dispRefPbps.
The anchor is an offset, not a re-scaling, and the slope is not free. per skew unit
is exactly what round-trip conservation admits, on both arms: with skew units per unit of coverage
and x-units per skew unit, the state advance must stay under the traverse’s, . The draining arm () binds at and the filling arm () at
, and both give . A piecewise map onto and
would have slopes and , which satisfy both bounds only at
; anywhere else one arm out-steps the traverse and a ping-pong trader harvests the
gap. Moving the anchor at the unchanged slope moves a level, which cancels exactly on a closed
loop, and the domain clamp only ever shortens a step. computeInventorySkew is untouched
(Inventory Management §3).
The addition is unchecked. That is sound because and computeInventorySkew
clamps to , bounding the sum to . The clamp, not the caller,
is what returns it to : a skew bound alone keeps in range only when
, and is a per-curve quantity, so the clamp is load-bearing rather than
defensive. Any new caller must still pass a clamped skew.
5.2. The book is asymmetric before anything else acts
splits the tradeable domain into a sell side of width and a buy side of width . At the reference skews (Inventory Management §3.3):
| Asset | Sell-side width | Buy-side width | ||
|---|---|---|---|---|
| USDC | 3400 | 3400 bp | 6600 bp | |
| USDT | 4050 | 4050 bp | 5950 bp | |
| DAI | 4750 | 4750 bp | 5250 bp | |
| RLUSD | 5350 | 5350 bp | 4650 bp | |
| USDG | 5450 | 5450 bp | 4550 bp |
(All five legs sit on antisymmetric presets, so and on these rows. On an asymmetric preset the same table would read .)
This asymmetry exists before any reserve cap, before the coverage toll, and before the fee. It is the intended behavior: an over-covered leg has little room left to be sold into and a lot of room to be bought out of.
5.3. Aggregating virtual depth across pools
Off-chain view only.
@btr-protocol/sdk(aggregateDepthCurves), consumed by the swap depth panel and the chart liquidity bands. The chain quotes one pool at a time; this is the venue-level book a router faces.
A pair can be quoted by more than one pool. USDT/USDC is quoted by both the stable pool and the volatile pool. Each pool carries its own coverage, hence its own , hence its own center (§5.1), hence its own skew mid . Two pools on the same pair do not share a mid, and the gap between their mids is the inter-pool arbitrage, not a spread.
Per pool the aggregator reads the quoted depth curve and keeps six quantities: the skew touch
/ (pre-fee, pre-toll, curve.bids[0].price / curve.asks[0].price), the same touch net
of fee and coverage toll / , the mid , the mark, the spread, and the
densified ladder. The weight is the pool’s total quoted size, .
The touch is an extremum, not a mean. A taker routes to one pool, the best one:
and identically on the net touch, , . The index sets , hold only the pools actually quoting that side, so a one-sided pool (reserve-clipped, §7) cannot drag the side it does not quote. A size-weighted touch, , prints a bid below the best bid and an ask above the best ask: a price no router accepts and no fill reaches. On a two-pool pair whose mids differ by , the weighted bid understates the executable bid by up to .
Size-weighting stays on the ladder and on the scalars. Behind the touch, sizes are additive: rungs from different pools that fall in the same price bucket are summed, , because a taker sweeping to price takes both. The venue mid, mark and spread are depth-weighted means, : they are ladder-centering and reference quantities, not executable prices. Mid answers “where is the book centered, and what is the inventory premium against the mark”, which is a venue-wide average; touch answers “what do I fill at”, which is a max.
Invariant: , with an empty side not binding. The weighted mean is clamped into the touch interval. The clamp binds only when the touch set differs from the mid set, which a one-sided pool causes: a heavy ask-only pool at next to a two-sided pool at leaves while the raw mean is . The invariant is what keeps the taker cost split (net touch measured from mid) signed correctly on both sides.
Pre-fee the aggregated touch can cross. Pre-fee each pool’s two sides meet at its own mid, , so with distinct mids . That crossing is real: it is the cross-pool arbitrage, worth per unit before costs. It is not a spread and is not printed as one. The executable statement is the net touch , , which crosses only when the mid dispersion exceeds the two crossing costs. Consumers gate on before drawing a cost band.
The ladder is priced on the NET basis. Each rung takes its own , the executable price at that rung’s own size (half the path spread plus the coverage toll evaluated there), not the skew price . Where a toll binds this widens the ladder with depth rather than shifting it by a constant. The skew touch , survives as a reference (the inventory premium against the mark, and grossing net sizes up); nothing draws it. Sizes stay gross: is not monotone into the coverage wall, so netting them would truncate the ladder exactly where depth matters.
One basis on the price axis is the point. The drawn quote is , ; a ladder priced on underneath it sits INSIDE that quote, and within a single pool, where , every rung lands between the drawn bid and the drawn ask. Measured on live AUDF/KRW1: 30 of 30 rungs inside the touch.
No rung is priced through the touch. Bucketing opens each pool’s ladder strictly beyond that pool’s own net touch (bid buckets floor below , ask buckets ceil above ), so every merged bid rung is below and every merged ask rung above . The max/min touch is what makes this hold: under a weighted mean the tightest pool’s rungs sit through the printed touch, showing size available at a price better than the best price.
The step resolves the ladder, not its distance from mid. The bucket width is chosen from each side’s rung EXTENT, touch to far end, taken per pool. Measuring it from instead folds in the gap between mid and the touch, which is zero only on the skew basis: on the net basis it pushed the step about too coarse and collapsed a 15-rung side to 4.
6. Dispersion dynamics
6.1. The dispersion contract
Each preset is fitted at a reference dispersion dispRefPbps stored in its header. A live quote scales the
offset linearly (Pricing._scaleY, Pricing.sol):
where is the stored pbps value, the live dispersion in pbps, the curve’s
dispRefPbps, and .
A linear y-scale preserves monotonicity and C2 exactly, so volatility can widen or tighten the curve with no refit. The preset fixes the shape and its reference half-swing; dispersion is the continuous scale applied to that shape at every quote.
Truncation direction matters here. Solidity integer division truncates toward zero, so a negative offset rounds up (toward zero, i.e. toward the mark) and a positive offset rounds down (also toward the mark). The scaled offset is therefore always at least as close to the mark as the exact value, by at most 1 pbps. That is pool-unfavorable on the sell side by under 0.0001%, and it is the reason the economic floors in §9 exist as an independent backstop rather than relying on the scale.
6.2. Live dispersion formula
Canonical. Every other statement of in the docs points here rather than restating the law.
Pricing._calculateDispersion (Pricing.sol):
(Pricing.sol; since the 2026-08-21 adaptive-dispersion change, which dropped a historic
damping that pinned every book at its quiet-tape floor.)
| Symbol | Field | Type | Unit |
|---|---|---|---|
| return value | uint32 | pbps | |
Asset.minDispersionPbps | uint32 | pbps, the quiet-tape floor | |
| ceiling | MAX_DISPERSION_PBPS | uint32 | pbps protocol constant = 900000 (PoolConstantsLib.sol). There is no per-asset ceiling field; Asset.maxDispersion does not exist |
FeedData.sigmaPbps | uint32 | PBPS (1e6 = 100%, so 10000 = 1%) | |
Asset.vegaBps | uint16 | basis 10000 |
The divisor is SC.BPS. At the slope is exactly one: dispersion tracks the feed’s σ
1:1 above the floor. Live vega is 1.0x on stable legs and on every pool’s hub, and below it on the
volatile classes — 0.30x FX, 0.40x crypto majors, 0.45x metals, 0.35x equities — so on those the
slope is that fraction of σ (Parametrization §4.2).
is the additive base, not a clamp applied after the fact: at the curve sits exactly at . Only the ceiling is a clamp. There is deliberately no fixed base dispersion: a hardcoded base would make tight stable bands of 1 to 6 bp unreachable in a quiet tape.
6.3. Volatility sensitivity
| (PBPS) | as % | (pbps) | |
|---|---|---|---|
| 76 | 0.0076% | 10000 | 76 |
| 999 | 0.0999% | 10000 | 999 |
| 5000 | 0.5% | 10000 | 5000 |
| 10000 | 1% | 10000 | 10000 |
| 50000 | 5% | 10000 | clamped at MAX_DISPERSION_PBPS − |
| 10000 | 1% | 5000 | 5000 |
6.4. Sigma is the live width driver at shipped vega
Deployed on stable legs and on every pool’s hub; the volatile classes were cut to 3,000-4,500 on 2026-09-04 to keep their books inside the interior swing cap (§6.5). At the formula collapses to pbps: dispersion tracks the feed’s σ one for one above the floor. Consequences:
- Every 1% of feed σ adds 10000 pbps - a full 1% of mark - to the leg’s band, on top of
minDispersionPbps. - Live USDT ( PBPS on oracle
0xd3fb...f0e4, feed0xe2ca...aef9; floor 161): the band is pbps, not the bare floor. - WETH (floor 2034, ): a 1% adds 4000 pbps, taking the quiet-tape band to 6034. At the pre-cut the same σ took it to 12034 — over the preset’s swing cap, which is why the class was cut.
- The only ceiling is
MAX_DISPERSION_PBPS(§6.2), reached at at . What a leg’s shape tolerates is governed by §6.5 below.
Both the floor and the slope are shaping parameters. minDispersionPbps sets the quiet-tape
width; sets how fast σ widens it. A model or simulation that treats dispersion as purely
floor-pinned describes the pre-adaptive-dispersion contract, not this one.
also drives the volatility term of the path spread and the staleness surcharge. See Spread & Fees.
6.5. Parameters
| Parameter | Type | Unit | Purpose |
|---|---|---|---|
vegaBps | uint16 | basis 10000 | Volatility sensitivity; 10000 everywhere deployed |
minDispersionPbps | uint32 | pbps | Additive base and quiet-tape floor; bound at the write under the preset’s fence cap |
MAX_DISPERSION_PBPS | uint32 | pbps | Protocol-wide read ceiling (§6.2). Not a parameter and not per-asset |
dispRefPbps | uint16 | pbps | Reference dispersion of the fit, in the curve header |
The preset cap bounds the write path, the protocol constant bounds the quote.
Pricing.dispersionCap returns with
PBPS, the widest dispersion whose
interior mid swing still fits the manipulation fence
(Anchor Path Pricing §3.2).
PoolConfig.sanitizeDispersion(minDispersionPbps, cap) (PoolConfig.sol) checks,
never clamps: it maps minDispersionPbps == 0 to the protocol default of 1000 pbps and
reverts Err.InvalidInput on any floor above the cap or above MAX_DISPERSION_PBPS, at
both write paths (initAsset and setProfile) - a rejected write rather than a silently
narrowed quiet-tape quote. The σ-driven term above the
floor is not capped by the preset: κ rides up to MAX_DISPERSION_PBPS, and if it ever exceeds
the shape’s own dispersionCap the interior swing reverts fail-closed (Err.Overflow) - a
σ-triggered outage on that leg, never a mispriced quote. Shipped caps, by the shape’s
span-to-dispRefPbps ratio:
| Preset | Ratio | dispersionCap (PBPS) |
|---|---|---|
| 1, 2, 5 | 2 | 5000 |
| 3 | 4 | 2500 |
| 4 | 10 | 1000 |
At (1x) the swing cap sits PBPS of σ above
the floor - about 0.48% of added σ on a ratio-2 stable (floor 161, cap 5000) and about 0.09% on a
ratio-10 preset; a class running below 1x buys headroom in exactly that ratio, which is the
live configuration on every volatile class - so a violent tape, not an operator, is what can push a leg into the fail-closed
revert. Past the cap the , CI and staleness terms of the spread keep widening regardless. setCurve’s “may not deepen a live preset” rule is exactly
non-increasing on a centered curve, so the cap is non-decreasing across any legal refit.
7. The depth denominator
Full statement: Inventory Management §4.
is the leg’s raw reserves, with a division guard and nothing else:
uint256 depth = reserves == 0 ? 1 : uint256(reserves);It is measured in the profile asset’s own token units. That is what makes the traversal anchor-free:
volumeFraction = amountIn * BPS / depth compares two quantities in the same token, so no price
enters the x-axis at all.
| Reserves | |
|---|---|
| 1 wei (division guard) | |
| , at any coverage |
Nothing on the depth axis is coverage-dependent. at every coverage, so the traversed interval, and therefore the impact charged, scales with and with nothing else. Coverage reaches the quote through the skew anchor (§5) and the convex coverage toll, both of which charge the drain. Why inflating as coverage falls is an LP leak: Inventory Management §4.
8. Traversal
8.1. Sell leg
Pricing._traverseCurve (Pricing.sol), selling = true (the profile asset is being sold
into its parent):
with the input amount in the profile asset’s own units.
and the traversal integrates .
8.2. Buy leg
selling = false (the parent is being sold to acquire the profile asset):
integrating . The buy leg needs in child units while amountIn arrives in parent
units, so _priceEdgeHop (Pricing.sol) sizes it in two steps: estimate the child amount at
the zero-volume mid, shift decimals, then run the real traverse on that estimate, then invert:
midPrice = _legMid(mark, dispersion, curve, skew) // zero-volume, at x0
estOut = amountIn * WAD / midPrice
estChild = estOut shifted by (child.decimals - parent.decimals)
execPrice = _traverseCurve(mark, dispersion, curve, header, x0, estChild, depth, selling=false, midPrice)
amountOut = amountIn * WAD / execPriceThe sizing is an estimate at the mid, so the buy leg’s realized is correct to first order in
trade size, not exactly. The sell leg has no such step: amountIn is already in profile-asset units.
8.3. Why round trips lose
The curve is nondecreasing, and the sell branch always integrates the interval below while the buy branch always integrates the interval above it. Therefore, for every size and every curve:
A sell executes at or below the mid, a buy at or above it, and the gap is monotone in size. There is no size, no skew and no dispersion at which a round trip through one leg gains. Combined with monotonicity (condition C2) and the single-canonical-integer rule for edge marks (condition C1), this extends to any closed cycle in the anchor tree. See Anchor Path Pricing.
8.4. The volume-fraction quantum
is an integer number of bps, so a trade smaller than has exactly zero price
impact: , width == 0, and the traversal returns the point value at
(Pricing.sol).
Live USDT (): the quantum is 6.0036 USDT. A 1 USDT swap traverses nothing and executes exactly at the mid. This is confirmed on-chain in §8.6. It is not a rounding artifact worth removing: the quantum is 0.01% of depth by construction, and any curve impact below it is smaller than 1 pbps at the shipped wall widths.
8.5. Average execution price
with from NUQuartic.areaQ.
Pricing.sol. The division by width happens before _scaleY, in pbps units, so
the intermediate keeps 9 decimal digits of headroom and the only meaningful truncation is the final
one, to whole pbps.
Units: areaQ returns pbps; dividing by the width in gives
pbps; _scaleY divides by and rescales by to give pbps; the price
formula divides by PBPS. Skipping any one of those three is a factor of ,
or respectively.
8.6. Worked example, verified on chain
Stable pool, USDT to USDC (upward leg, USDT sells into its base anchor). Inputs, all read live at capture time:
| Input | Value | Source |
|---|---|---|
| 60,035.652 | getAsset, so | |
SwapQuote.skewIn (Inventory Management §3.3 snapshot) | ||
| 4050 | , | |
| preset | 2 () | shipped deployment parameters (2. Deployments) |
| 76 PBPS | oracle feed | |
| 237 pbps | at USDT’s | |
| mark | 0.9993376994 | SwapQuote.markPrice |
The measured column below was captured on chain before the adaptive-dispersion change dropped the historic damping: at capture time the band sat at the bare floor (, read as ) and the read skew was the old symmetric-slope value (, ). Under the current law the same live inputs give and (), so these rows verify the traversal pipeline against the pre-adaptive-dispersion contract; they are kept as the historical record, not as today’s quote.
Predicted at the capture-time inputs (, ), compared to Pool.getSwapQuote
on the pre-adaptive-dispersion deployment:
amountIn | (bps) | interval | predicted | predicted | measured |
|---|---|---|---|---|---|
| 1 | 0 | point at 3050 | pbps | 0 | 0.0 pbps |
| 1,000 | 166 | pbps | pbps | pbps | |
| 10,000 | 1665 | pbps | pbps | pbps |
Re-predicted at HEAD inputs (, , same live and ) with the
bit-exact integer evaluation of the stored preset (the Rust reference implementation’s
eval_q/area_q,
the same math Pricing.sol runs):
amountIn | (bps) | interval | predicted | predicted |
|---|---|---|---|---|
| 1 | 0 | point at 4050 | pbps | 0 |
| 1,000 | 166 | pbps | pbps | |
| 10,000 | 1665 | pbps | pbps |
where is the traversal factor and the mid is the zero-volume price at
. In the historical rows the predicted mid offset of pbps sat against a measured
midPrice/markPrice of pbps. The residuals are the integer truncations in _scaleY and in
the area division, each under 1 pbps, as expected; both prices are exact WAD, so none of the residual
is an encoding artifact.
8.7. Pipeline
1. Read oracle feed -> mark, sigma (Pricing._readOracle)
2. kappa = min(maxDisp, minDisp + sigma*vega/BPS) (_calculateDispersion)
3. psi = f(reserves, liabilities) (computeInventorySkew, fixed law)
4. D = reserves == 0 ? 1 : reserves (inline)
5. x0 = clamp(median + 50*psi, 0, BPS) (_skewToDepth; median from header)
6. vf = min(BPS, amountIn*BPS/D)
7. [lo,hi] = selling ? [x0-vf, x0] : [x0, x0+vf], clamped to [0, BPS]
8. curve = PoolStorage.curves[asset.presetId]
9. yBar = _scaleY(areaQ(lo,hi)/(hi-lo), header, kappa)
10. p = mark*(PBPS + yBar)/PBPS, floored (section 9)8.8. Implementation signature
function _traverseCurve(
uint256 mark,
uint32 dispersionPbps,
NUQ.Curve storage curve,
uint256 header,
uint256 startDepthBps,
uint256 amountIn,
uint256 depth,
bool selling,
uint256 buyMidHint // reuse of the sizing mid on the zero-width buy path; 0 = none
) internal view returns (uint256 avgPrice)Pricing.sol.
9. The price floor
Every price path shares one economic backstop, applied in Pricing._flooredOffsetPrice:
| Backstop | Value | Meaning |
|---|---|---|
SPLINE_MIN_OFFSET_PBPS | Averaged offset never discounts below , i.e. price never falls below 10% of the mark |
_flooredOffsetPrice runs on the area path, the zero-width path and the buy-mid path alike, so a
mutated or degenerate curve cannot quote below 10% of the mark on any route. It is internal rather
than private so test_flooredOffsetPrice_* pins the clamp directly rather than through a curve
fixture, and test_flooredOffsetPrice_clamps_at_ten_pct_mark asserts the value.
9.1. One pricing law
The traversal has one pricing law, and it is the only one an edge can be priced under, because
there is no state in which a listed asset lacks a curve: PoolConfig.validatePresetAssign refuses the
listing (§2.2), so every leg that can be quoted carries a preset from its first block.
That is a security property, not a convenience. A second pricing law reachable on any edge is the exact defect the interior fence exists to prevent (Anchor Path Pricing §3.1): an edge terminal on one route and interior on another, priced two ways, is an atomic cycle with no manipulation and no risk. Refusing the listing closes it by construction rather than by care.
The same argument covers span. A clamped degree-4 knot vector makes evalQ exact at both ends, so
span > 0 follows from _validate with no rounding hypothesis
(testFuzz_rangeQ_is_exact_at_both_clamped_ends) and needs no guard branch. A guard there would be a
branch no mutation could kill, and one returning a zero swing would leave an interior leg carrying no
fence at all.
10. Governance
Two timelocked levers, both LOW tier, both guardian-cancellable:
- Shared curve install or refit.
Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_CURVE), bytes32(uint256(presetId)), abi.encode(interior, wQ, dispRefPbps, flags))thenexecuteSetCurve(pool, presetId). Mutating a preset that live assets reference is the periodic-refit path. Full validation (monotone weights, knot ordering, segment cap, coefficient bounds) runs at execute.FLAG_REQUIRES_WALLcannot be flipped on an in-use preset: that would strand referencing assets (PoolConfig.sol). - Asset repoint.
Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_PROFILE), bytes32(uint256(uint160(token))), payload)thenexecuteUpdateProfile. Pricing-shape only: reserves, liabilities and coverage are untouched. The wall gate and curve existence are checked at execute (PoolConfig.validatePresetAssign).
Stripping from an asset holding a wall-gated preset is rejected at the risk-config
write (PoolConfig.sol), so the gate cannot be evaded from the other side.
Bootstrap: before sealing, Admin.setCurve and addAsset install curves directly.
sealBootstrap(pool) permanently closes that path, after which only the timelocked route exists.
11. Gas
11.1. Curve operations (cold, measured)
| Operation | SLOADs | Gas |
|---|---|---|
evalQ | 3 | ~5.4k |
areaQ over any range | 5 | ~11.2k |
Integration cost is flat in trade size and in segments crossed.
11.2. Swap, end to end (cold)
| Route | Gas |
|---|---|
| base to spoke | ~168.9k |
| spoke to base | ~168.5k |
| spoke to spoke | ~198.5k |
12. Concentration versus other DEXs
Every incumbent AMM hard-codes one liquidity density through its invariant. AIMM carries no invariant: the on-chain object is , the integral of a chosen monotone C2 density, so the density is a design input rather than a consequence of a formula.
12.1. Resolution
- Offset resolution. AIMM prices offsets in pbps: 1 pbps = 0.0001% = 0.01 bp. Uniswap v3/v4
quantize to ticks; the finest is 1 bp (
tickSpacing1 on the 0.01% tier). AIMM offset resolution is about 100x finer, and the half-swing is continuous and can sit below 0.5 bp, where no v3 position can exist. The live USDT half-swing of 1.61 bp is not representable as a single v3 tick. - Within-tick shape. Inside a v3 tick, density is uniform: a flat slab. AIMM density is a smooth flat-topped quartic table.
12.2. Density model comparison
| DEX class | Density model | Concentration control | Tails |
|---|---|---|---|
| Constant product (Uniswap v2, Balancer weighted) | One fixed hyperbola set by the invariant | None | Unbounded, to 0 and |
| Curve StableSwap | One global curvature scalar , same shape for every asset | Single scalar | Unbounded |
| Gyroscope E-CLP | Fixed parametric family (ellipse: , , rotation) | Bounded but locked to one family; no multimodal, fat-tail or asymmetric density | Bounded to the ellipse |
| Uniswap v3/v4 | Stack of flat ticks approximating a shape | Many LP positions; floors at 1 tick; no sub-tick | Per-position, flat within tick |
| BTR AIMM | Chosen monotone C2 density, integrated to offset(depth) | One shape, per-asset width, hot-swappable under timelock; continuous below 0.5 bp | Hard-cut at by construction |
12.3. Bounded tails
is an explicit two-sided bound: 100% of the leg’s liquidity sits within of the mark, by construction of the q70 truncation. Constant-function market makers cannot do this: their invariants have unbounded support and always quote some depth across the entire price axis, spending capital on offsets that never fill. AIMM cuts both tails.
The corollary is that AIMM’s book exhausts. Past there is nothing: clamps at BPS, clamps at the domain edge, and the quote stops improving for the taker.
12.4. One machine, wide or narrow
The same machinery spans the full concentration range without changing the engine or the shape. An
operator picks wide or narrow per asset by pointing at a codebook entry and
letting minDispersionPbps scale it continuously. The live ladder runs from 1.61 bp (USDT) to 37.40 bp
(KRW1), a 23x range, across three stored shape vectors.
13. Width selection
The shape family is fixed. The only per-asset choice is width: the ratio of the
codebook entry the asset points at, plus the asset’s own minDispersionPbps.
| Asset class | (bp) | dispRefPbps (pbps) | Preset |
|---|---|---|---|
| Tight-peg stables, coverage-walled | 1 | 100 | 2 |
| Wider stables and newer pegs | 2 | 100 | 3 |
| Widest stables | 5 | 100 | 4 |
| Volatiles, gold, FX | 5 | 500 | 5 |
| Base numeraire (no wall) | 1 | 100 | 1 |
Derive width from observed density, not from a class label. The preset assignment, the dispersion band and the fee floors are read off the asset’s measured return density over a multi-week window of NXR history, never from a class label or a volume ranking. The mapping is mechanical: upper-tail move size selects the codebook entry and drives the
maxDeviationfloor, realized and depeg volatility grade the fee floor, and an asset whose tail is too heavy to admit a finite-variance fit is barred from the tightest entry however tight its central core looks. The table above is a summary of where the shipped legs landed, not the rule that put them there. The shipped generator records its own rule in the published deployment parameters: from the two-week fee-kernel density, which is why the roster’s majors land near feeQ99 (19 to 21 bp, sominDispersionPbps1900 to 2100 at preset 5) rather than at the q70 shape cut near 11 bp. A pool-creation wizard surfaces the measured density and its implied width; it does not ask the operator to guess a shape, because the shape is not a choice. The estimators, windows and quantile cutoffs are internal calibration; the outputs are not secret, since every parameter lands in on-chain state and in the published deployment parameters. Assets with no clean tape hold their prior values until the history exists.
14. Related documentation
- Inventory Management: coverage, skew and depth, the three inputs
- Anchor Path Pricing: marks, leg orientation and multi-hop composition
- Spread & Fees: the fee charged on top of the traversal
- Slippage & Price Impact: the full decomposition and taker guidance
- Parametrization: tuning guide
- Invariants: the properties the curve must preserve