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, C2 at every knot, integrable in constant time at any trade size.


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. The curve carries no reference to the 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. Control weights are validated nondecreasing (NUQuarticLib.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 curve table and assets point at one (§2.2). A curve is immutable once installed: a refit installs a fresh id and the legs repoint to it (§10).

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

NUQuarticLib.sol. The words live in one SSTORE2 blob per (pool, curveId), deployed at the CREATE3 address NUQuarticLib.ptr(curveId) and never rewritten: one header word plus two words per segment, at most 14 segments (MAX_SEGS, NUQuarticLib.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]opaque curve key, set by the fit and never read on chain
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 reads 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 (NUQuarticLib.median, NUQuarticLib.dispRefOf), not by hand-written shifts. flags has no accessor: no contract reads it. It is an off-chain codebook key the fit stamps onto the entry.

2.2. Curve table and asset pointer

Canonical for the codebook rationale. Parametrization §9.2 and the Overview summarise it only.

Curves are shared per pool, one SSTORE2 blob per curveId (§2.1). Each asset carries Asset.curveId (uint16). curveId = 0 is the explicit no-shape sentinel and is refused at config: PoolConfigLib.validateCurveAssign 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. A stable and a volatile share a curve id when they need the same width; the presets carry distinct wQ vectors and are not interchangeable rescalings of one another (§4.3). The preset each leg carries is listed in pool composition.

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:

  • 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, ...) / executeUpdateCurve, §10).
  • Re-pointing an asset is cheap and frequent. Asset.curveId 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 cost 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. A handful of entries cover many legs because those legs disagree about width far more than about shape.

2.3. Validation at install

NUQuarticLib._validate (NUQuarticLib.sol) and NUQuarticLib.pack (NUQuarticLib.sol):

PropertyRequirement
dispRefPbps≠0 (it is a divisor in _scaleY; zero would brick every quote)
wQ length n5≤n, n-4≤14
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, pack 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 (NUQuarticLib._centre, NUQuarticLib.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 any curve whose closed-form fit is 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. NUQuarticLib.pack 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)≤0≤y(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 PricingLib._skewToDepth anchors zero inventory skew on (§5.1).

At assignment time PoolConfigLib.validateCurveAssign (PoolConfigLib.sol) checks exactly one thing: header == 0 reverts NotConfigured(ASSET, token). curveId = 0 is the empty sentinel and setCurve refuses to install a shape there, so a curveless asset is unconstructible.

There is no flag-conditional wall gate, on this path or any other, and none is needed. The wall is unconditional: every κ writer calls requireNeverDepletable, which holds kappaCovBps between a floor and BPS on every listed asset including the hub, so no asset can exist without a wall for a gate to condition on. The coupling between a codebook entry and the wall it was fitted against is an off-chain property, enforced by the fit and the keeper pre-submit fence (Parametrization §10.3).

The price multiplier’s positivity needs no check of its own: sanitizeDispersion holds the asset’s minDispersionPbps under PricingLib.dispersionCap on an interior-capable leg (one that another leg anchors to; PoolConfigLib.legDispersionCap), and under MAX_DISPERSION_PBPS on every other leg. The cap is MAX_INTERIOR_SWING_PBPS expressed in the curve’s own units, so the deepest offset the shape can quote at that floor is half the swing cap, 5431 against PBPS’s 1e6. MAX_DISPERSION_PBPS = 900000 (90% of PBPS) is the separate hard bound (PoolConstantsLib.sol, clamped in PricingLib._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+1≥wi. 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 (NUQuarticLib.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 (NUQuarticLib.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

NUQuarticLib.evalQ (NUQuarticLib.sol): locate the segment from the header, copy two words, Horner on five coefficients.

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

One cold account plus two EXTCODECOPY reads (header, then the segment’s two words). 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)

NUQuarticLib.areaQ (NUQuarticLib.sol) returns 0 when x1≥x2, otherwise differences two _at calls. _at (NUQuarticLib.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. One cold account and at most three word copies (header plus two words 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 (NUQuarticLib.sol):

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


4. Curve: central-normal plateau

The shipped shapes. The 7-weight bell_* presets are central-normal plateaus. The 9-weight _inv complements (Curve presets) invert that density: thin at the mark, deep in the wings.

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 (bell_100)
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 flat. Measured local slopes on the shipped curve (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 words.

The model behind every codebook entry is the one closed form of §4.1 evaluated at each half-swing, 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. Preset weight vectors

Control weights are ±H·Q scaled: wQ[0] = -H_pbps * 1e9, wQ[6] = +H_pbps * 1e9. The 7-weight presets share interiorB = [1314, 8686] and differ in H and dispRefPbps:

PresetHdispRefPbps (pbps)H/dispRef
bell_1001 bp = 100 pbps1001
bell_5005 bp = 500 pbps5001

Ids are assigned per deployment (pool composition); the full preset list, including the 9-weight complements, is on Curve presets. The two vectors, in pbps·Q:

bell_100 = [-100000000000, -90053719349, -38039704575, 0, 38039704575, 90053719349, 100000000000] bell_500 = [-500000000000, -450268596744, -190198522877, 0, 190198522877, 450268596744, 500000000000]

Two distinct wQ vectors, not one shape scaled. They are not exact multiples of each other, because each is an independently-rounded fit of the §4.1 closed form:

RelationExact multiple of bell_100Stored in bell_500Residual
5× bell_100[1]-450,268,596,745-450,268,596,7441 ULP
5× bell_100[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 (§6.5; at most 54.31× on bell_100), so the residual stays under 2×10-7 pbps against a 1 pbps quote granularity: it does not on its own move a quoted mid.

It matters for a different reason. The preset vectors are canonical on-chain state, so a vector regenerated by rescaling another will not reproduce the stored bytes and will fail parity against on-chain state and against the research parity vectors. Treat the presets as independent curves:

  • Do not derive one from another.
  • Re-fit and compare against the stored values rather than rescale when refreshing an entry.

The ratio H/dispRef is what sets a curve’s width at quote time. The live half-swing of the book is

y~(BPS)=Hρ·κ

so with dispersion κ in pbps, bell_100 and bell_500 (and the 9-weight complements) reach exactly κ pbps at the edge.

4.4. Quiet-tape half-swings

The quiet-tape (σ=0) edge is (H/dispRef)· minDispersionPbps. Under the adaptive-dispersion law every leg quotes above its floor whenever σ>0 (§6.4). Per-asset floors: pool composition.

4.5. Gas

Harness: NUQuarticSetGas.t.sol, measuring NUQuarticLib.pack plus the SSTORE2 blob deploy by segment count and by curve. There is no update path: an id is written once. The figures below are recorded run values, not assertions in the benchmark, so re-measure before quoting them externally.

ShapemWordsInstall
Central-normal plateau (shipped)37~410,000

Cost is linear in segment count: two words per segment plus the header, written once and read as one cold account plus two or three word copies 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

PricingLib._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 curve, x*=5000)Meaning
-1000Maximum discount, over-covered leg
0x* = 5000Center, quote at the mark
+10010000Maximum premium, under-covered leg

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: antisymmetric curves on symmetric knots have x*=5000 exactly, 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. The error is up to half the peak-to-peak swing.

The anchor is an offset, not a re-scaling. The slope is bounded. BPS/200 per skew unit is 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·s≤BPS/c. The draining arm (k=200) binds at c=1 and the filling arm (k=100) at c→2, and both give s≤BPS/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 extracts 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. On an antisymmetric curve (x*=5000, so x0=5000+50ψ):

ψx0Sell-side widthBuy-side width
-3035003500 bp6500 bp
-1045004500 bp5500 bp
+1055005500 bp4500 bp
+3065006500 bp3500 bp

On an asymmetric curve the same table would read x*+50ψ.

This asymmetry exists before any reserve cap, before the coverage toll, and before the fee. It is intended: an over-covered leg has less room to be sold into and more room to be bought out of.

Several pools can quote one pair, each with its own ψ and so its own mid. The off-chain depth aggregation across them: Quotes & Routing §8.4.


6. Dispersion dynamics

6.1. The dispersion contract

Each curve is fitted at a reference dispersion dispRefPbps stored in its header. A live quote scales the offset linearly (PricingLib._scaleY, PricingLib.sol):

y~=⌊yQ·κρQ⌋→0

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

PricingLib._calculateDispersion (PricingLib.sol):

κ=min(MAX_DISPERSION_PBPS,κmin+⌊σ·νBPS⌋) (PricingLib.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. Below it the slope is ν/BPS 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 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 ν=10000 the formula collapses to Δκ=⌊σ⌋ pbps: dispersion tracks the feed’s σ one for one above the floor. Consequences:

  • Every 1% of feed σ (10000 PBPS) adds 10000ν/BPS pbps: 10000 pbps (a full 1% of mark) at ν=10000, 5000 at ν=5000.
  • A leg with floor 200 and σ=76 PBPS at ν=10000 quotes a band of 200+76=276 pbps, not the bare floor.
  • The only ceiling in general is MAX_DISPERSION_PBPS (§6.2), reached at σ≈89.8% at ν=10000 for a small floor. On an interior-capable leg the shape’s own cap in §6.5 binds first.

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
minDispersionPbpsuint32pbpsAdditive base and quiet-tape floor; bound at the write under the curve’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 curve cap bounds the write path, the protocol constant bounds the quote. PricingLib.dispersionCap returns cap·dispRef·Q/span with cap=MAX_INTERIOR_SWING_PBPS=10,862 PBPS, the widest dispersion whose interior mid swing still fits the manipulation fence (Anchor Path Pricing §3.2). PoolConfigLib.sanitizeDispersion(minDispersionPbps, cap) (PoolConfigLib.sol) checks, never clamps: it maps minDispersionPbps == 0 to the protocol default of 1000 pbps and reverts Err.InvalidInput on any floor above the leg’s cap (the curve cap on an interior-capable leg, MAX_DISPERSION_PBPS otherwise), 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 curve: κ 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. The cap scales with the shape’s span-to-dispRefPbps ratio span/(Q·dispRef): ratio 2 gives 5431 PBPS, ratio 10 gives 1086, and a preset whose edge sits at k times dispRefPbps divides that by k. Every preset on Curve presets has ratio 2.

At ν=10000 (1x) the swing cap sits cap-κmin PBPS of σ above the floor: about 0.52% of added σ on a ratio-2 leg (floor 200, cap 5431). The narrower the shape’s cap-to-floor gap, the sooner it closes. A ν below 1x gains headroom in that ratio. Past the cap the σ, CI and staleness terms of the spread keep widening. A refit is a fresh id: setProfile checks the leg’s minDispersionPbps against the new curve’s cap at the repoint, so no legal repoint lowers a live leg’s cap below its floor.


7. The depth denominator

D is the leg’s raw reserves, so the traversed interval scales with 1/R and with nothing else. Definition, guard and why coverage never reaches this axis: Inventory Management §4.


8. Traversal

8.1. Sell leg

PricingLib._traverseCurve (PricingLib.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. 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 (PricingLib.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. No size, skew or dispersion lets a round trip through one leg gain. 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 (PricingLib.sol).

For D=R=60,000 units the quantum is 6 units: a 1-unit swap traverses nothing and executes exactly at the mid (§8.6). The quantum is 0.01% of depth, and any curve impact below it is smaller than 1 pbps at the narrow widths of pegged legs, so it is not worth removing.

8.5. Average execution price

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

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

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

On the sell leg the result is capped at the mark: psell¯=min(p¯,m) (_traverseCurve). Below parity the skew lifts the mid above the mark, and the pool never buys above it. The buy leg is uncapped.

8.6. Worked example

Illustrative inputs, a stable leg selling into its base anchor:

InputValueSource
R=D60,035.652c>1 so D=R
ψ-19SwapQuote.skewIn
x04050x*+50ψ, x*=5000
presetbell_100 (H/dispRef=1)
σ76 PBPSfeed
κ237 pbps161+⌊76·ν/BPS⌋=161+76 at ν=10000

Predicted with the bit-exact integer evaluation of the stored curve (the Rust reference implementation’s eval_q/area_q, the same math PricingLib.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. Against Pool.getSwapQuote the residuals are the integer truncations in _scaleY and in the area division, each under 1 pbps; both prices are exact WAD, so none of the residual is an encoding artifact.

8.7. Pipeline

1. Read oracle feed -> mark, sigma (_loadMarks, mark words) 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 = NUQuarticLib.ptr(asset.curveId) (SSTORE2 blob) 9. yBar = _scaleY(areaQ(lo,hi)/(hi-lo), header, kappa) 10. p = mark*(PBPS + yBar)/PBPS, floored (section 9) 11. p = selling ? min(p, mark) : p (pool never buys above mark)

9. The price floor

Every price path shares one economic backstop, applied in PricingLib._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: PoolConfigLib.validateCurveAssign refuses the listing (§2.2), so every leg that can be quoted carries a curve from its first block.

This is a security property. 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 at config time.

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 guardian-cancellable: the curve install at LISTING (1 day), the repoint at TUNING (1 hour):

  1. Shared curve install. Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_CURVE), bytes32(uint256(curveId)), abi.encode(interior, wQ, dispRefPbps, flags)) then executeUpdateCurve(pool, curveId). Full validation (monotone weights, knot ordering, segment cap, coefficient bounds) runs at execute. An installed id is immutable: setCurve reverts InvalidInput when its blob exists (PoolConfigLib.setCurve). A refit installs under a fresh id, and the legs move with lever 2.
  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. Curve existence and the leg’s minDispersionPbps against the new curve’s ceiling are checked at execute (PoolConfigLib.setProfile).

κcov cannot be stripped from any asset: requireNeverDepletable floors it on every writer, so there is no un-walled state to reach from either 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

OperationReads (one cold account)
evalQheader + 1 segment copy
areaQheader + 1 segment copy with both ends in one segment, else + 2

Integration cost is flat in trade size and in segments crossed. Cold gas for both, and for a full swap: Pool §12.


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. A half-swing of 1.6 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

12.3. Bounded tails

H is an explicit two-sided bound: 100% of the leg’s liquidity sits within ±H·κ/dispRef of the mark, from 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.

AIMM’s book exhausts: past ±H, 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: edge width is the preset’s ratio times the floor, so a few stored shape vectors cover the whole range.


13. Width selection

The installed presets, their numbers and charts, and a selection guide are on Curve presets.

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.

Derive width from observed density, not from a class label. The curve assignment, the dispersion band and the fee floors are read off the asset’s measured return density over a multi-week window of the off-chain mark pipeline’s 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 maxDevBps 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 generator records its rule in the published deployment parameters: Sdep=max(q70(|ℓ|),2θ,feeQ99) from the two-week fee-kernel density. A pool-creation wizard surfaces the measured density and its implied width; the shape is not an operator input. 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.