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
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. 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.
- 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. - 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 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).
| 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] | opaque curve key, set by the fit and never read on chain |
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
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
. 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.curveIdis 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 cost 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. 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):
| 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, pack shifts the whole
polygon by so that , i.e. (NUQuarticLib._centre, NUQuarticLib.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 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 ,
so is the integral of a nonnegative density running from to and
the single with 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 ), 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 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
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 (NUQuarticLib.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 . (NUQuarticLib.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
NUQuarticLib.evalQ (NUQuarticLib.sol): locate the segment from the header, copy two words, Horner
on five coefficients.
One cold account plus two EXTCODECOPY reads (header, then the segment’s two words). 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
NUQuarticLib.areaQ (NUQuarticLib.sol) returns 0 when , otherwise differences two
_at calls. _at (NUQuarticLib.sol) is the stored prefix plus the local quintic primitive:
Result units are pbps. 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):
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 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 (bell_100) |
|---|---|---|
| 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 flat. Measured local slopes on the shipped curve (): 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. words.
The model behind every codebook entry is the one closed form of §4.1 evaluated at each half-swing, 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. Preset weight vectors
Control weights are scaled: wQ[0] = -H_pbps * 1e9, wQ[6] = +H_pbps * 1e9. The 7-weight
presets share interiorB = [1314, 8686] and differ in and dispRefPbps:
| Preset | dispRefPbps (pbps) | ||
|---|---|---|---|
bell_100 | 1 bp = 100 pbps | 100 | 1 |
bell_500 | 5 bp = 500 pbps | 500 | 1 |
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:
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:
| Relation | Exact multiple of bell_100 | Stored in bell_500 | Residual |
|---|---|---|---|
bell_100[1] | 1 ULP | ||
bell_100[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 (§6.5; at most on bell_100), so the
residual stays under 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 is what sets a curve’s width at quote time. The live half-swing of the book is
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 () edge is minDispersionPbps. Under the
adaptive-dispersion law every leg quotes above its floor whenever (§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.
| Shape | Words | Install | |
|---|---|---|---|
| Central-normal plateau (shipped) | 3 | 7 | ~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 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:
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 curve, ) | Meaning | |
|---|---|---|
| 0 | Maximum discount, over-covered leg | |
| = 5000 | Center, quote at the mark | |
| 10000 | Maximum premium, under-covered leg |
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 : antisymmetric curves on symmetric knots have exactly, 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. The error is up to half the peak-to-peak swing.
The anchor is an offset, not a re-scaling. The slope is bounded. per skew unit
is 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 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 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 . On an antisymmetric curve (, so ):
| Sell-side width | Buy-side width | ||
|---|---|---|---|
| 3500 | 3500 bp | 6500 bp | |
| 4500 | 4500 bp | 5500 bp | |
| 5500 | 5500 bp | 4500 bp | |
| 6500 | 6500 bp | 3500 bp |
On an asymmetric curve the same table would read .
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):
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 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):
(PricingLib.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. Below it the
slope is 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 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 the formula collapses to pbps: dispersion tracks the feed’s σ one for one above the floor. Consequences:
- Every 1% of feed σ (10000 PBPS) adds pbps: 10000 pbps (a full 1% of mark) at , 5000 at .
- A leg with floor 200 and PBPS at quotes a band of pbps, not the bare floor.
- The only ceiling in general is
MAX_DISPERSION_PBPS(§6.2), reached at at 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
| Parameter | Type | Unit | Purpose |
|---|---|---|---|
vegaBps | uint16 | basis 10000 | Volatility sensitivity |
minDispersionPbps | uint32 | pbps | Additive base and quiet-tape floor; bound at the write under the curve’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 curve cap bounds the write path, the protocol constant bounds the quote.
PricingLib.dispersionCap returns with
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 : ratio 2 gives 5431 PBPS, ratio 10 gives 1086, and a preset whose edge sits at times
dispRefPbps divides that by . Every preset on Curve presets has ratio 2.
At (1x) the swing cap sits 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
is the leg’s raw reserves, so the traversed interval scales with 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):
with the input amount in the profile asset’s own units. 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 (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 / 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. 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
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
(PricingLib.sol).
For 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
with from NUQuarticLib.areaQ.
PricingLib.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.
On the sell leg the result is capped at the mark: (_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:
| Input | Value | Source |
|---|---|---|
| 60,035.652 | so | |
SwapQuote.skewIn | ||
| 4050 | , | |
| preset | bell_100 () | |
| 76 PBPS | feed | |
| 237 pbps | at |
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):
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
. 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:
| 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: 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):
- Shared curve install.
Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_CURVE), bytes32(uint256(curveId)), abi.encode(interior, wQ, dispRefPbps, flags))thenexecuteUpdateCurve(pool, curveId). Full validation (monotone weights, knot ordering, segment cap, coefficient bounds) runs at execute. An installed id is immutable:setCurverevertsInvalidInputwhen its blob exists (PoolConfigLib.setCurve). A refit installs under a fresh id, and the legs move with lever 2. - 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. Curve existence and the leg’sminDispersionPbpsagainst the new curve’s ceiling are checked at execute (PoolConfigLib.setProfile).
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
| Operation | Reads (one cold account) |
|---|---|
evalQ | header + 1 segment copy |
areaQ | header + 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 , 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. 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 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 |
12.3. Bounded tails
is an explicit two-sided bound: 100% of the leg’s liquidity sits within 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 , 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: 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 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
maxDevBpsfloor, 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: 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.
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
- Curve presets: every installed preset with charts, and how to choose one
- Parametrization: tuning guide
- Invariants: the properties the curve must preserve