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

y:[0,BPS]pbps·Q,ynondecreasing

mapping cumulative depth x (in bps of the leg’s reserves, so x[0,10000]) to a relative price offset y (in pbps, stored at Q=109 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:

p=m·PBPS+y~PBPS

with m the oracle mark and y~ the offset after dispersion scaling (§6.1).

Four properties follow:

  1. The curve is anchor-free. x is a fraction of depth in the profile asset’s own token units (§7), y 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.
  2. 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.
  3. The density is C2. Degree 4 with simple interior knots gives y 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).
  4. 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).

FieldBitsMeaning
mheader[0:8]Segment count, 1 to 14
b_1..b_13header[8:216]Interior right boundaries in x, uint16 each
medianheader[216:232]Density median x* in bps, the curve’s own zero (§5.1)
dispRefPbpsheader[232:248]Reference dispersion in pbps that the fit was built at
flagsheader[248:256]bit 0 = FLAG_REQUIRES_WALL
c_0..c_4segs[2i], segs[2i+1] lowPower-basis coefficients on local u[0,1], int64, pbps·Q
Sjsegs[2i+1] highExact prefix integral 0bjydx, int128, pbps·Q·x

The directory holds the m-1 interior boundaries only. The last, bm, is always BPS: 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 dispersion/dispRef. 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.presetId is a uint16 pointer, so as an asset’s observed density drifts the keeper moves it to a different codebook entry through setProfile without touching any curve.

The price of quantization is the residual between an asset’s own fit and its nearest codebook entry. The continuous dispersion/dispRef 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):

PropertyRequirement
dispRefPbps0 (it is a divisor in _scaleY; zero would brick every quote)
wQ length n5n, n-414
interior lengthexactly n-5
Non-flatwQ[n-1] != wQ[0]
MonotonewQ[i] >= wQ[i-1] for all i
Interior knotsstrictly increasing, in (0,BPS)
Coefficients$

Segment count is m=n-4. 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 (wQ[0]+wQ[n-1])/2 so that y(0)+y(BPS)=0, i.e. β=y(0)/span-1/2 (NUQuartic._centre, NUQuartic.sol). Clamped endpoints make y(0)=wQ[0] and y(BPS)=wQ[n-1], so the transform is one subtraction per weight. It is shape-preserving (the fitted density y 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 y(0)=-span/2, so y is the integral of a nonnegative density running from -span/2 to +span/2 and the single x* with y(x*)=0 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 y(0)0y(BPS)), 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 x is integer bps, |y(x*)| is bounded by one x-unit of density rather than by zero; on an antisymmetric curve it is exactly zero and x*=BPS/2. 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 == 0 reverts NotConfigured(ASSET, token). presetId = 0 is the empty sentinel and setCurve refuses to install a shape there, so a curveless asset is unconstructible.
  • A preset with FLAG_REQUIRES_WALL may only be assigned to an asset with kappaCovBps != 0 (PoolConfig.sol, reverts BadConfig).

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

y(x)=iwiBi(x)

with clamped quartic B-splines Bi, monotonicity of y is equivalent to wi+1wi. That is a linear constraint on the coefficients, checkable exactly on-chain in O(n) 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 u[0,1]:

y(u)=c0+c1u+c2u2+c3u3+c4u4

c0,c1,c2 come from the left jet of the span (value, first and second derivative from de Boor), and c3,c4 from the right-end value and slope:

c1=y(ts)·hD,c2=y'(ts)·h22D,A=y(ts+1)-c0-c1-c2,B=y(ts+1)·hD-c1-2c2

c3=4A-B,c4=B-3A

with h=ts+1-ts. D=106 (NUQuartic.sol) is an extra fixed-point scale carried through the derivative pyramids: slope truncation feeds c2=s0h2/2, so a 1-unit error in s0 would cost h2/2 units of y, which is 1.25×107 at a 5000-unit span. D=106 caps that residual at about 13 units of pbps·Q, i.e. 1.3×10-8 pbps.

Value, slope and y' 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.

u=(x-x0)·Ph,P=1018

3 cold SLOADs total (header plus two segment slots). u is clamped to [0,P] by if (dx > h) dx = h.

3.4. Exact O(1) integration

VWAP needs x1x2ydx. Each segment stores the exact prefix integral to its left edge, so

x1x2ydx=S(x2)-S(x1)

NUQuartic.areaQ (NUQuartic.sol) returns 0 when x1x2, otherwise differences two _at calls. _at (NUQuartic.sol) is the stored prefix plus the local quintic primitive:

S(x)=Si+hP(c0u+c1u22+c2u33+c3u44+c4u55)

Result units are pbps·Q·x. 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):

segydx=h(60c0+30c1+20c2+15c3+12c4)60


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 N(0,σg) truncated at the empirical q70, the exact 30% folded-tail cut. The depth axis x is the CDF of that distribution, so the offset curve is its quantile function:

y(x)=H·Φ-1(0.15+0.7xBPS)Φ-1(0.85),Φ-1(0.85)1.03643

H 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:

x (bps)Closed form, H=100Shipped spline (preset 1/2)
0-100-100.0000
1314-67.51-67.6849
2500-43.78-43.7827
500000.0000
7500+43.78+43.7827
10000+100+100.0000

(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 H.)

The density is dx/dy, whose reciprocal dy/dx=0.7H/(φ(Φ-1(·))Φ-1(0.85)) is minimized at x=5000. 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 (H=100): 0.0246 pbps/bp on [0,1314], 0.0202 on [1314,2500], 0.0185 on [2500,3050], 0.0172 on [3050,5000].

4.2. Structure

m=3 segments, two interior knots at x={1314,8686} bracketing the mark at ±0.7σ. The middle segment carries the flat top, the two outer segments carry the shoulder roll-off. 3×2+1=7 storage slots.

The model behind all five entries is the one closed form of §4.1 evaluated at three half-swings, and the ratio H/dispRef 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 ±H·Q scaled: wQ[0] = -H_pbps * 1e9, wQ[6] = +H_pbps * 1e9.

IDHwQ vectordispRefPbps (pbps)H/dispRefWall-gatedSeeded from
11 bp = 100 pbpsA1001NoUSDC
21 bp = 100 pbpsA1001YesUSDT
32 bp = 200 pbpsB1002YesUSDE
45 bp = 500 pbpsC1005YesUSDG
55 bp = 500 pbpsC5001NoWETH

All five share interiorB = [1314, 8686] and a 7-element weight vector. The three distinct vectors, in pbps·Q:

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:

RelationExact multiple of AStoredResidual
2× A[2] against B[2]-76,079,409,150-76,079,409,1511 ULP
5× A[1] against C[1]-450,268,596,745-450,268,596,7441 ULP
5× A[2] against C[2]-190,198,522,875-190,198,522,8772 ULP

One ULP is 10-9 pbps at the fitted scale. _scaleY multiplies by κ/dispRef before truncating to whole pbps, and that amplification is bounded by dispersionCap at 10x (§6.5), so the residual reaches at most 2×10-8 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 H/dispRef is what sets a preset’s width at quote time. The live half-swing of the book is

y~(BPS)=Hρ·κ

so with dispersion κ in pbps, presets 1, 2 and 5 reach exactly κ pbps at the edge, preset 3 reaches 2κ, preset 4 reaches 5κ.

4.4. Quiet-tape half-swings

Using deployed minDispersionPbps. These are the quiet-tape (σ=0) widths; under the adaptive-dispersion law every leg quotes above its floor whenever σ>0, one-for-one at shipped vega (§6.4):

AssetPresetH/dispRefminDispersionPbpsQuiet-tape edge ±
USDC11200200 pbps = 2.00 bp
USDT21161161 pbps = 1.61 bp
USDG4573365 pbps = 3.65 bp
DAI325591118 pbps = 11.18 bp
USDTB21470470 pbps = 4.70 bp
WETH5120342034 pbps = 20.34 bp
XAUT5115001500 pbps = 15.00 bp (was 3601; cut 2026-09-04 with the metal class)
KRW15137403740 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.

ShapemSlotssetCurve update (warm)First set (cold)
Central-normal plateau (shipped)37~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 m=3 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:

x0=clamp(x*+ψ·BPS200,0,BPS)=clamp(x*+50ψ,0,10000)

x* is the curve’s own density median, read from the header (§2.1, §2.3). It is the one x at which the curve quotes the mark itself, so zero inventory skew quotes the mark for every shape.

ψx0 (antisymmetric preset, x*=5000)Meaning
-1000Maximum discount, pool is over-covered and wants to sell
0x* = 5000Center, quote at the mark
+10010000Maximum premium, pool is under-covered and wants to buy

One definition of center, held on both sides of the write. _centre pins y(0)=-span/2 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 BPS/2 is not the anchor and only coincides with it on an antisymmetric wQ: all five shipped presets are antisymmetric on symmetric knots, so x*=5000 exactly on each, but anchoring on the midpoint would quote y(BPS/2)0 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. BPS/200 per skew unit is exactly what round-trip conservation admits, on both arms: with k skew units per unit of coverage and s x-units per skew unit, the state advance must stay under the traverse’s, k·sBPS/c. The draining arm (k=200) binds at c=1 and the filling arm (k=100) at c2, and both give sBPS/200. A piecewise map onto [0,x*] and [x*,BPS] would have slopes (BPS-x*)/100 and x*/100, which satisfy both bounds only at x*=BPS/2; 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 x*BPS and computeInventorySkew clamps ψ to [-100,+100], bounding the sum to [-5000,15000]. The clamp, not the caller, is what returns it to [0,BPS]: a ±100 skew bound alone keeps x0 in range only when x*=BPS/2, and x* 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

x0 splits the tradeable domain [0,BPS] into a sell side of width x0 and a buy side of width BPS-x0. At the reference skews (Inventory Management §3.3):

Assetψx0Sell-side widthBuy-side width
USDC-3234003400 bp6600 bp
USDT-1940504050 bp5950 bp
DAI-547504750 bp5250 bp
RLUSD+753505350 bp4650 bp
USDG+954505450 bp4550 bp

(All five legs sit on antisymmetric presets, so x*=5000 and x0=5000+50ψ on these rows. On an asymmetric preset the same table would read x*+50ψ.)

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 x0=x*+50ψ (§5.1), hence its own skew mid mp=priceAt(x0). 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 p the aggregator reads the quoted depth curve and keeps six quantities: the skew touch bp / ap (pre-fee, pre-toll, curve.bids[0].price / curve.asks[0].price), the same touch net of fee and coverage toll bp~ / ap~, the mid mp, the mark, the spread, and the densified ladder. The weight is the pool’s total quoted size, wp=sizes.

The touch is an extremum, not a mean. A taker routes to one pool, the best one:

b=maxpBbp,a=minpAap

and identically on the net touch, b~=maxbp~, a~=minap~. The index sets B, A 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, bpwp/wp, 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, S(π)=pSp(π), because a taker sweeping to price π takes both. The venue mid, mark and spread are depth-weighted means, m¯=mpwp/wp: 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: bm¯a, 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 mp=2.0 next to a two-sided pool at 1.0 leaves a=1.0 while the raw mean is 1.9. 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, bp=ap=mp, so with distinct mids b=maxmp>minmp=a. That crossing is real: it is the cross-pool arbitrage, worth b-a per unit before costs. It is not a spread and is not printed as one. The executable statement is the net touch b~, a~, which crosses only when the mid dispersion exceeds the two crossing costs. Consumers gate on a~>b~ 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 b, a survives as a reference (the inventory premium against the mark, and grossing net sizes up); nothing draws it. Sizes stay gross: cum·m 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 b~, a~; a ladder priced on π underneath it sits INSIDE that quote, and within a single pool, where bp=ap=mp, 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 bp~, ask buckets ceil above ap~), so every merged bid rung is below bp~b~ and every merged ask rung above ap~a~. 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 m¯ 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 5× 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):

y~=yQ·κρQ0

where yQ is the stored pbps·Q value, κ the live dispersion in pbps, ρ the curve’s dispRefPbps, and Q=109.

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):

κ=min(MAX_DISPERSION_PBPS,κmin+σ·νBPS) (Pricing.sol; since the 2026-08-21 adaptive-dispersion change, which dropped a historic σ/1000 damping that pinned every book at its quiet-tape floor.)

SymbolFieldTypeUnit
κreturn valueuint32pbps
κminAsset.minDispersionPbpsuint32pbps, the quiet-tape floor
ceilingMAX_DISPERSION_PBPSuint32pbps protocol constant = 900000 (PoolConstantsLib.sol). There is no per-asset ceiling field; Asset.maxDispersion does not exist
σFeedData.sigmaPbpsuint32PBPS (1e6 = 100%, so 10000 = 1%)
νAsset.vegaBpsuint16basis 10000

The divisor is SC.BPS. At ν=10000 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).

κmin is the additive base, not a clamp applied after the fact: at σ=0 the curve sits exactly at κmin. 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

Δκ=σνBPS

σ (PBPS)σ as %νΔκ (pbps)
760.0076%1000076
9990.0999%10000999
50000.5%100005000
100001%1000010000
500005%10000clamped at MAX_DISPERSION_PBPSκmin
100001%50005000

6.4. Sigma is the live width driver at shipped vega

Deployed ν=10000 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 ν=10000 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 (σ=76 PBPS on oracle 0xd3fb...f0e4, feed 0xe2ca...aef9; floor 161): the band is 161+76=237 pbps, not the bare floor.
  • WETH (floor 2034, ν=4000): a 1% σ adds 4000 pbps, taking the quiet-tape band to 6034. At the pre-cut ν=10000 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 σ89.8% at ν=10000. 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

ParameterTypeUnitPurpose
vegaBpsuint16basis 10000Volatility sensitivity; 10000 everywhere deployed
minDispersionPbpsuint32pbpsAdditive base and quiet-tape floor; bound at the write under the preset’s fence cap
MAX_DISPERSION_PBPSuint32pbpsProtocol-wide read ceiling (§6.2). Not a parameter and not per-asset
dispRefPbpsuint16pbpsReference dispersion of the fit, in the curve header

The preset cap bounds the write path, the protocol constant bounds the quote. Pricing.dispersionCap returns cap·dispRef·Q/span with cap=INTERIOR_SWING_CAP_PBPS=10,000 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:

PresetRatio span/(Q·dispRef)dispersionCap (PBPS)
1, 2, 525000
342500
4101000

At ν=10000 (1x) the swing cap sits cap-κmin 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 span/dispRef 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.

D 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.

ReservesD
R=01 wei (division guard)
R>0R, at any coverage

Nothing on the depth axis is coverage-dependent. D=R at every coverage, so the traversed interval, and therefore the impact charged, scales with 1/R and with nothing else. Coverage reaches the quote through the skew anchor x0 (§5) and the convex coverage toll, both of which charge the drain. Why inflating D 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):

vf=min(BPS,q·BPSD),x1=max(0,x0-vf)

with q the input amount in the profile asset’s own units.

and the traversal integrates [x1,x0].

8.2. Buy leg

selling = false (the parent is being sold to acquire the profile asset):

x1=min(BPS,x0+vf)

integrating [x0,x1]. The buy leg needs vf 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 / execPrice

The sizing is an estimate at the mid, so the buy leg’s realized vf 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 x0 while the buy branch always integrates the interval above it. Therefore, for every size and every curve:

ysell¯y(x0)ybuy¯

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

vf is an integer number of bps, so a trade smaller than D/BPS has exactly zero price impact: vf=0, width == 0, and the traversal returns the point value at x0 (Pricing.sol).

Live USDT (D=R=60,035.65): 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

p¯=m·PBPS+y¯PBPS,y¯=A(x1,x2)x2-x1·κρQ

with A(x1,x2)=x1x2ydx from NUQuartic.areaQ.

Pricing.sol. The division by width happens before _scaleY, in pbps·Q 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·Q·x; dividing by the width in x gives pbps·Q; _scaleY divides by Q and rescales by κ/dispRef to give pbps; the price formula divides by PBPS. Skipping any one of those three is a factor of 109, κ/dispRef or 106 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:

InputValueSource
RUSDT=D60,035.652getAsset, c>1 so D=R
ψ-19SwapQuote.skewIn (Inventory Management §3.3 snapshot)
x04050x*+50ψ, x*=5000
preset2 (W/dispRef=1)shipped deployment parameters (2. Deployments)
σ76 PBPSoracle feed
κ237 pbps161+76·ν/BPS=161+76 at USDT’s ν=10000
mark0.9993376994SwapQuote.markPrice

The measured column below was captured on chain before the adaptive-dispersion change dropped the historic σ/1000 damping: at capture time the band sat at the bare floor (κ=161, read as 161+76/1000) and the read skew was the old symmetric-slope value (ψ=-39, x0=3050). Under the current law the same live inputs give κ=237 and x0=4050 (ψ=-19), 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 (κ=161, x0=3050), compared to Pool.getSwapQuote on the pre-adaptive-dispersion deployment:

amountInvf (bps)intervalpredicted y¯predicted Γ-1measured Γ-1
10point at 3050-54.09 pbps00.0 pbps
1,000166[2884,3050]-56.52 pbps-2.43 pbps-2.0 pbps
10,0001665[1385,3050]-79.59 pbps-25.50 pbps-25.0 pbps

Re-predicted at HEAD inputs (κ=237, x0=4050, same live R 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):

amountInvf (bps)intervalpredicted y¯predicted Γ-1
10point at 4050-38 pbps0
1,000166[3884,4050]-41 pbps-3 pbps
10,0001665[2385,4050]-72 pbps-34 pbps

where Γ=p¯/mid is the traversal factor and the mid is the zero-volume price at x0. In the historical rows the predicted mid offset of -54.09 pbps sat against a measured midPrice/markPrice of -54.00 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:

BackstopValueMeaning
SPLINE_MIN_OFFSET_PBPS-0.9·PBPSAveraged offset never discounts below -90%, 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:

  1. Shared curve install or refit. Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_CURVE), bytes32(uint256(presetId)), abi.encode(interior, wQ, dispRefPbps, flags)) then executeSetCurve(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_WALL cannot be flipped on an in-use preset: that would strand referencing assets (PoolConfig.sol).
  2. Asset repoint. Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_PROFILE), bytes32(uint256(uint160(token))), payload) then executeUpdateProfile. Pricing-shape only: reserves, liabilities and coverage are untouched. The wall gate and curve existence are checked at execute (PoolConfig.validatePresetAssign).

Stripping κcov 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)

OperationSLOADsGas
evalQ3~5.4k
areaQ over any range5~11.2k

Integration cost is flat in trade size and in segments crossed.

11.2. Swap, end to end (cold)

RouteGas
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 offset(depth), 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 (tickSpacing 1 on the 0.01% tier). AIMM offset resolution is about 100x finer, and the half-swing H 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 classDensity modelConcentration controlTails
Constant product xy=k (Uniswap v2, Balancer weighted)One fixed hyperbola set by the invariantNoneUnbounded, to 0 and
Curve StableSwapOne global curvature scalar A, same shape for every assetSingle scalar AUnbounded
Gyroscope E-CLPFixed parametric family (ellipse: a, b, rotation)Bounded but locked to one family; no multimodal, fat-tail or asymmetric densityBounded to the ellipse
Uniswap v3/v4Stack of flat ticks approximating a shapeMany LP positions; floors at 1 tick; no sub-tickPer-position, flat within tick
BTR AIMMChosen monotone C2 density, integrated to offset(depth)One shape, per-asset width, hot-swappable under timelock; H continuous below 0.5 bpHard-cut at ±H by construction

12.3. Bounded tails

H is an explicit two-sided bound: 100% of the leg’s liquidity sits within ±H·κ/dispRef 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 ±H there is nothing: vf clamps at BPS, x 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 (H,dispRef) 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 H/dispRef ratio of the codebook entry the asset points at, plus the asset’s own minDispersionPbps.

Asset classH (bp)dispRefPbps (pbps)Preset
Tight-peg stables, coverage-walled11002
Wider stables and newer pegs21003
Widest stables51004
Volatiles, gold, FX55005
Base numeraire (no wall)11001

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 maxDeviation floor, 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: Sdep=max(q70(||),2θ,feeQ99) from the two-week fee-kernel density, which is why the roster’s majors land near feeQ99 (19 to 21 bp, so minDispersionPbps 1900 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.