Configuration parameters
Every knob a pool carries, with its units, the formula it feeds, and the bound that constrains it. Also the map of who may write what: four lanes with different authority and different latency, and a fence set that decides which of them can move a parameter without a timelock. Quantities that are not tunable are marked as such; several of the dials operators expect to find are protocol constants.
Per-deployment values: 2. Deployments. Units: PBPS (), BPS (), WAD (), or raw.
1. Contents
- Who may set what
- Asset configuration
- Sensitivities: vega
- Dispersion
- Risk configuration
- Oracle configuration
- Fee parameters
- Pricing curves
- The fence set
- The fee floor covers 2 theta
- Reference parameter table
- Units reference
- Known gaps
2. Who may set what
Six lanes write parameters, each with its own authority and bounds. Every parameter belongs to exactly one primary lane.
| Lane | Authority | Bound | Latency |
|---|---|---|---|
| Owner, immediate | _onlyPoolAdmin | AdminParamsLib.set defensive-tighten test must hold, plus the armed minFeeHardMinPbps floor | 0 |
| Owner, timelocked | _onlyPoolAdmin | none beyond validation | 1 hour to 7 days by op |
| Steward, bounded | perms bit 18 (PERM_RISK_STEWARD) holder; the owner is excluded | RiskFences hard bounds + a 24 h cumulative lowering envelope | 0 |
| Steward, one-way | perms bit 18 holder, or the owner | direction only: the write must reduce risk against live state | 0 |
| Guardian, halts | perms bit 17 (PERM_GUARDIAN) holder | haltAsset / batchRiskOp halts and the one-way coop clear (haltAsset src 0x0800); no risk-config write | 0 |
| Keeper, off-chain | keeper config only | never written on chain | n/a |
The one-way rule. A write that only reduces risk executes instantly and carries no magnitude bound; the same field in the risk-up direction queues at the tier of its consequence: risk numbers at TUNING, listings and hooks at LISTING, structure at GOVERNANCE.
| Lever | Risk down: instant, by | Risk up: queued |
|---|---|---|
swap / liability-swap / flash switch (*_ENABLED_BIT), each on its own | clear: steward, owner (setRiskConfigTighten) | set: UPDATE_RISK, TUNING |
deposit gate (DEPOSIT_GATED_BIT) | arm: steward, owner (setRiskConfigTighten) | clear: UPDATE_RISK, TUNING |
coop fill switch (COOP_ENABLED_BIT) | clear: steward, owner (setRiskConfigTighten); guardian, owner, seat (haltAsset src 0x0800) | set: UPDATE_RISK, TUNING |
coop terms (coopDiscBps, coopRebateBps) | disc down / rebate up: owner (requestAssetParams), steward inside fences | disc up / rebate down: TUNING; steward inside maxDeltaBps/24 h; never past the coop fences |
kappaCovBps | raise: steward, owner (setRiskConfigTighten) | cut: UPDATE_RISK, TUNING |
depositCapCode, maxLiabWeightBps | lower: steward, owner (setRiskConfigTighten) | raise or disable: UPDATE_RISK, TUNING |
minDispersionPbps | raise: steward, owner (raiseMinDispersion) | cut: UPDATE_PROFILE, TUNING |
minFeePbps | raise: owner (requestAssetParams), steward inside fences | cut: TUNING; steward inside maxDeltaBps/24 h |
vegaBps | raise: owner (requestAssetParams), steward inside fences | cut: TUNING; steward inside maxDeltaBps/24 h |
refBandBps (depeg breaker) | narrow, not below MIN_REF_BAND_BPS (50): steward, owner (narrowRefBand) | widen: UPDATE_ANCHOR, GOVERNANCE |
flowCooldownSecs | raise: owner (setFlowCooldown) | cut: UPDATE_FEES, TUNING |
| yield hook | remove: steward, owner (clearAssetHook: sync, recall, drop; recall-only on a remainder) | attach or replace: UPDATE_HOOK, LISTING |
| hook buffer / credit cap | lower: owner (YieldHook) | raise: LISTING / TUNING |
| halt (asset) | halt: guardian, owner, or the pool’s seat | unhalt: owner, instant (deliberate exception); the seat only once the guardian’s notice (LISTING delay from the first halt) has run |
| halt (mark-store lane) | halt: guardian, owner, steward (halt(lane, tierMask)) | unhalt: owner, instant |
A same-asset withdraw has no switch: only a halt stops it. A cross-asset withdrawTo is a liability swap plus a same-asset exit, so it needs LIABILITY_SWAP_ENABLED_BIT on both legs. Deposits have no per-leg off bit: arming the gate restricts a leg to allowlisted depositors, instantly.
The direction test runs against state read in the same call, so there is no request-to-execute gap for it to go stale in. Two moves stay timelocked regardless of direction because neither is comparable in one dimension: a curve repoint changes shape, and a liquidity floor raise gates LP exit. A compromised steward or guardian can cause denial of service, not a loosening: it can only tighten. The owner revokes a steward instantly by clearing its bit (AccessControl.setPerms(who, PERM_RISK_STEWARD, false)), and the tightened values unwind on the ordinary queue.
Owner-immediate is not unbounded. Admin.requestAssetParams applies a write immediately only if bootstrapSealed[pool] is false, or if AdminParamsLib.set classifies it as a defensive tighten:
minLiquidityunchangedminFeePbpsnon-decreasingvegaBpsnon-decreasingcoopDiscBpsnon-increasing andcoopRebateBpsnon-decreasing
Anything else queues at TUNING with a request-time snapshot of the five fields (minLiquidity, minFeePbps, vegaBps, coopDiscBps, coopRebateBps); at execute a field the op moves that moved underneath reverts AssetParamMoved. Independently, an armed minFeeHardMinPbps fence floors even the owner path: lowering below a fence requires two transactions, setRiskFences first.
The instant lane needs no ceiling of its own. The two-sided minFeePbps bound of §3.1 applies on every write path whichever lane the write arrives on, so it binds the untimelocked lane as tightly as the timelocked one. Raising it would need a lane-specific bound shipped with it.
A vega raise is instant on the owner lane. On an interior-capable leg live dispersion is minDispersionPbps + σ·vegaBps/BPS against the curve’s PricingLib.dispersionCap, and a route through the leg fails closed above it. PoolConfigLib.setAssetParams therefore refuses a raise that breaches the cap at the leg’s live σ, on every lane: the same write-time guard a floor raise gets from setProfile. σ can still rise after the write. A leaf leg is clamped at MAX_DISPERSION_PBPS and owes the cap nothing.
Never auto-moved. minLiquidity is owner-only: setAssetParamsBounded reverts outright on any change to it.
RiskConfig follows the same asymmetry. setRiskConfigTighten is the one-way lane above, the one entry for every RiskConfig field (a κ raise is the live config with κ swapped): steward or owner, instant when every field tightens. Any other direction is a queued UPDATE_RISK, and a queued depositCapCode raise is bounded at 10× per op (§6).
Every instant write voids the queued op that would undo it. Queued payloads are absolute, so an op authored before a tighten restores the old value on execute. setRiskConfigTighten voids a live UPDATE_RISK that is looser on any field; raiseMinDispersion a lower queued floor; setFlowCooldown a shorter queued cooldown in UPDATE_FEES; narrowRefBand clamps a wider band in a queued UPDATE_ANCHOR that keeps the live reference feed to the narrowed one in place (QueuedRefBandClamped; a reference move or a disarm is left alone), so the 7-day op keeps its eta and a steward never vetoes a re-anchor; clearAssetHook voids a queued install. A queued op at least as tight survives and the tighten still lands. The void checks read queued words raw, never abi.decode, and key on the exact length requestOp pins per op (an opType with no length entry admits only an empty payload), so an out-of-range payload cannot wedge a lane; execute decodes, so it cannot land either. A vega raise needs no void: a queued op that moves vega fails its compare-and-swap. A queued feed widen fails its own bounds check once a tighten lands under it.
Timelocked operations. All are queued by the generic Admin.requestOp(pool, opType, subject, payload) and cancelled by Admin.cancelOp(pool, opType, subject); only the execute side is named per op. opType is IPool.OpType, subject is the left-padded token address for token-keyed ops, the curve id for UPDATE_CURVE, and ignored for the four pool-wide ops (MIGRATE_BASE_TOKEN, UPDATE_TREASURY, UPDATE_FEES, UPDATE_POOL_ADMIN). Delays are deploy-time data: a packed ConstantsLib.Tier schedule handed to the AccessControl constructor, read via ConstantsLib.delayOf. ConstantsLib.PROD_DELAYS is GOVERNANCE 7d, LISTING 1d, TUNING 1h.
opType | Tier | Delay | Execute | Effect |
|---|---|---|---|---|
UPDATE_PROFILE | TUNING | 1 hour | executeUpdateProfile | repoint curveId and the dispersion band |
UPDATE_RISK | TUNING | 1 hour | executeUpdateRisk | RiskConfig: flags, kappaCovBps, depositCapCode, maxLiabWeightBps |
UPDATE_FEES | TUNING | 1 hour | executeUpdateFees | protoSharePct, flashFeePbps, flowCooldownSecs |
ADD_ASSET | LISTING | 1 day | executeAddAsset | list a new leg |
UPDATE_CURVE | LISTING | 1 day | executeUpdateCurve | install a shared curve under a new id (an installed id is immutable) |
UPDATE_HOOK | LISTING | 1 day | executeUpdateHook | per-asset yield hook target and flags |
UPDATE_TREASURY | GOVERNANCE | 7 days | executeUpdateTreasury | fee-sink address |
SWEEP | GOVERNANCE | 7 days | executeSweep | recover an unlisted token to treasury() |
UPDATE_ANCHOR | GOVERNANCE | 7 days | executeUpdateAnchor | re-anchor, atomic with the oracle config |
MIGRATE_BASE_TOKEN | GOVERNANCE | 7 days | executeMigrateBaseToken | base token |
BACKFILL_LEGS | GOVERNANCE | 7 days | executeBackfillLegs | arm pool-level solvency on a pre-roster clone |
UPDATE_POOL_ADMIN | GOVERNANCE | 7 days | acceptPoolAdmin | pool-admin seat handover |
There is no oracle repoint op: OracleConfig is written at listing and by UPDATE_ANCHOR, and names a store lane, not an address; the store moves only with the Pool implementation, through the fleet upgrade (PoolFactory.executeReferenceUpgrade, GOVERNANCE, which calls migrateFrom before any pool moves).
UPDATE_ASSET_PARAMS is queued only by requestAssetParams itself and is not requestable through requestOp; it lands via executeUpdateAssetParams.
Immediate owner functions: addAsset and setCurve (pre-seal only), requestAssetParams, setRiskConfigTighten, raiseMinDispersion, narrowRefBand, clearAssetHook, setRiskFences, haltAsset / unhaltAsset (HALT_BIT), batchRiskOp, setFlowCooldown (raise-only), collapseAnchor, sealBootstrap; treasury only: claimTreasury; steward: setAssetParamsBounded, setRiskConfigTighten, raiseMinDispersion, narrowRefBand, clearAssetHook; guardian: haltAsset, batchRiskOp (halt), collapseAnchor, cancelOp (the last two on protocol pools only; on a seated pool cancelOp is the seat’s).
Re-anchoring is never instant: a re-anchor is a re-rooting, so UPDATE_ANCHOR queues at the GOVERNANCE base-migration tier and carries the oracle config in the same inseparable payload. The only instant anchor move is the guardian’s collapseAnchor, which may move a leg toward the root only, and halts it in the same call.
3. Asset configuration
IPool.Asset, stored at PoolStorage.assets[token], written by Admin.addAsset / requestAssetParams. Three storage slots. The packing is a quote-path decision: a quote reads exactly two cold Asset slots per leg, and the whole pricing shape (band, curve, fee floor, vega, decimals) arrives in one of them.
| Field | Type | Units | Meaning |
|---|---|---|---|
reserves | uint128 | token wei | tokens the pool holds; also the traverse depth denominator |
liabilities | uint128 | token wei | LP claim, raised by deposits and LP fees |
anchor | address | - | parent in the anchor tree; 0 is not a sentinel, every listed leg has one |
minLiquidity | uint96 | token wei | executable-reserve floor; swaps below it revert |
liquidityIndexWad | uint72 | WAD | share-to-value index; LIQUIDITY_INDEX_INIT_WAD = 1e18 |
minDispersionPbps | uint32 | PBPS | dispersion floor and additive base; checked at the write under the curve’s dispersionCap. There is no per-asset ceiling field (§5) |
curveId | uint16 | pointer | id of the pool’s shared curve; 0 is refused at config, not a fallback |
minFeePbps | uint16 | PBPS | per-leg spread floor at |
vegaBps | uint16 | base 10,000 | volatility sensitivity |
depositCapCode | uint16 | m<<4|e | deposit cap, m·10^e whole base tokens of leg notional; binds at credit time only (§6) |
decimals | uint8 | - | bound to the token at listing, not configurable; decimals != 0 is the not-configured sentinel |
deadSeedPow10 | uint8 | power of ten | dead-share seed in the token’s own unit; 0 = decimals default, bounded at decimals + 3 |
flags | uint16 | bitfield | feature and halt bits (§6.1) |
kappaCovBps | uint16 | bps | convex coverage-wall strength (§6.2) |
maxLiabWeightBps | uint16 | bps of the claim book | soft cap on the leg’s share of the pool claim book at credit time; 0 = disabled (§6) |
oracleBits | uint8 | bitfield | store lane (bits 0..5), INTERNAL (bit 6), UOA (bit 7); the packed OracleConfig (§7) |
refBandBps | uint16 | bps | reference band; bits 12..15 carry the depeg code instead (DEPEG_MASK) |
flags, kappaCovBps, depositCapCode and maxLiabWeightBps are the RiskConfig quartet: IPool.RiskConfig is an ABI and
memory type only, and all four fields are stored here, in Asset slot 2, not in a mapping of
their own. The inventory skew takes no per-asset field (Inventory Management §3), and the peg used by INTERNAL mode is a constant in the synthetic feed rather than a stored field (§7).
3.1. Config-time validation
Enforced on every write path (PoolConfigLib.sol):
| Rule | Rationale |
|---|---|
minFeePbps >= MIN_FEE_PBPS (= 1) | 1 PBPS = 0.01 bp is the finest on-chain quantum |
minFeePbps <= ONE_PCT_PBPS (= 10,000) | global write bound; replaces the per-asset ceiling and the instant-lane one at once (§2) |
every listed asset including the hub: 50 <= kappaCovBps <= 10000 | PoolConfigLib.requireNeverDepletable; _covToll is output-only so hub κ prices taking the hub out |
| a spoke’s κ never exceeds its hub’s | PoolConfigLib.requireHubKappaDominant: the hub is the output leg of every stale-spoke drain |
depositCapCode >> 4 != 0 | a zero mantissa is refused on every writer; 0 only survives on a pre-upgrade leg and means uncapped |
maxLiabWeightBps <= 10000 | bps of the claim book |
| an asset may not be listed without a curve | one pricing law per edge; there is no shapeless fallback |
minDispersionPbps <= 900000 (MAX_DISPERSION_PBPS) and non-zero → default 1000 | keeps _calculateDispersion’s floor meaningful and the clamp branch well-defined (PoolConfigLib.sanitizeDispersion) |
minDispersionPbps <= PricingLib.dispersionCap(curve) on an interior-capable leg; <= MAX_DISPERSION_PBPS otherwise | checked, never clamped: a floor past the cap configures a route outage at every σ, zero included, because an interior leg’s swing would fail closed (§5) |
3.2. Liquidity floor
minLiquidity is enforced on executable liquidity (), pre-outflow, against amountOut + minLiquidity:
uint256 need = q.amountOut + aOut.minLiquidity;
uint256 liq = aOut.reserves > inv ? uint256(aOut.reserves) - inv : 0; // post-recall
if (liq < need) revert Err.InsufficientAmount(liq, need);A fully hook-invested asset can hold minLiquidity and still revert every swap. Full form: Invariants I-14.
3.3. Depeg bounds live on the oracle config, not on the asset
A leg’s depeg protection is OracleConfig.refBandBps: it measures the leg’s primary mark against the same lane’s reference (R) tier and halts the swap past the band (§7). Where the mark is USD-attested or the pair is meant to trade at 1, the leg’s depeg code (RiskConfig.flags bits 12 to 15, band = code x 250 bps, set through UPDATE_RISK) adds a parity test against 1.0 and reverts PriceOutsideRefBand(mark, WAD); it runs inside the reference-band guard, so it needs a nonzero refBandBps; the base’s code sets its BaseDepegged band, 2,500 bps at code 0. A parity bound on a ratio such as stETH/ETH would be a number against a moving unit of account, so such a leg keeps code 0. The discretionary “stop quoting this leg” call is the guardian halt: instant, owner-reversible, and not a number anyone has to keep current.
4. Sensitivities
4.1. Inventory skew: not a parameter
PricingLib.computeInventorySkew takes (reserves, liabilities) and nothing else, and returns an int8 in by both clamp and type. There is no slope to tune and no saturation point to declare: the two arm slopes and both clamps are protocol constants, so no write can reprice a live book by moving them, and the asymmetry between the arms is the round-trip conservation bound. Formula: Inventory Management §3.1.
Why those constants. Writing the saturation points as , continuity requires and round-trip conservation requires , so is the unique admissible value and there is no dial to expose. The two bounds, the measured leak across a discontinuous clamp and the real-tape evidence that the clamp region is unreachable are at Inventory Management §3.2.1.
The skew anchor. maps onto the spline as , where is the curve’s stored density median (§9), not the domain midpoint. The median enters as an offset at the slope and leaves computeInventorySkew untouched, because conservation binds the slope on both arms.
4.2. Vega: volatility sensitivity
One parameter, two outputs, both clamped. Base 10,000 = 1.0x.
Dispersion (PricingLib._calculateDispersion):
Spread (PricingLib._pathSpread):
Vega semantics. Both and the spread are in PBPS and is in BPS, so means dispersion widens by the full () while the spread’s volatility term is : the spread prices 1 % of at 1.0x, 6.5 % at the uint16 ceiling . The gap between the two divisors is the shipped scale for the phase-1 stable core, where the fee floor carries the spread and the term is a tail adjustment; the divisor is rescaled, if at all, together with the volatile-core fit, never on its own. In _pathSpread, over the endpoints only, and it is the one risk input that is not composed per leg (every other one, in quadrature and the fee, CI and staleness terms by sum, is); in _calculateDispersion it is the profile asset’s own . Endpoint-max is correct there because is the pool’s own per-asset sensitivity dial, not a per-leg risk quantity, and the endpoints are the assets paid in and out.
There is no hardcoded base in either formula. The dispersion base is the per-asset minDispersionPbps, the spread base is the path fee floor. Hardcoding a 1000-PBPS dispersion base would make tight stable bands (1-6 bp) unreachable at .
also scales the spread’s slope at a smaller divisor, so cutting it to fit the swing cap (§5) takes the volatility premium down with it. One field serves two consumers; no value resolves that (§4.2.1).
4.2.1. Heterogeneous vega across assets
Nothing relates two assets’ vegaBps, and the owner and steward lanes each move one asset at a time (§2). Scopes, the inversion on composite paths and its live bound: Spread & Fees §11.2. The fence window that bounds it: §10.1.
4.3. No directional term
Every spread term is symmetric: no momentum or trend surcharge, and nothing in the fee model reads the direction of the last trade. Residual staleness is priced by the symmetric (Spread & Fees §3.4).
5. Dispersion
Dispersion is the price half-width over which liquidity is deployed, in PBPS. It rises with off the per-asset fitted floor = minDispersionPbps, one-for-one at and at of that elsewhere, and is clamped above by the protocol constant MAX_DISPERSION_PBPS, a read-path ceiling, not a per-asset field. Law and constant: Liquidity Shaping §6.2.
The curve’s fence cap bounds the floor at the write. PricingLib.dispersionCap is the widest dispersion whose interior mid swing the manipulation fence can bound, with PBPS, and PoolConfigLib.sanitizeDispersion checks minDispersionPbps against it at both write paths (initAsset, setProfile; revert, not clamp) for an interior-capable leg; a leaf is held only under MAX_DISPERSION_PBPS until a child anchors under it. There is no unset-curve case: an asset cannot be listed without a curve. The cap follows from the curve header, so it differs by curve: 5431 PBPS where span is twice Q·dispRef, less on a wider shape. Per-deployment values are in 2. Deployments. Silently narrowing minDispersionPbps would move the asset’s own quiet-tape quote, so a floor above the cap is a config error (InvalidInput). A σ-driven κ above the cap is not prevented - it fails closed on the interior swing (Err.Overflow) instead of ever mispricing.
The cap is a peak-to-peak swing (1.0862% of mark total, not a one-sided 1%); the fence, its constants and its ceilings: Anchor Path Pricing §3.2.
The swing-cap check changes no configured value: it refuses only floors that could never quote safely. Above the floor, σ itself drives κ toward the protocol ceiling: once reaches the curve’s cap an interior route through the leg fails closed, and a lower gains headroom in that ratio (§4.2); the , CI and staleness terms of the spread keep widening. An installed curve id is immutable, so the cap under a validated asset never moves; a new shape is a new id, and setProfile revalidates each asset against it when it is repointed.
The deployed band is bounded below: the risk keeper enforces a hard floor on every band it emits:
is how far the mark may walk before a push triggers; is the excursion past measured over one push interval, so is its 99th percentile, how far the mark gets before the push lands; is charged inside the band and therefore consumes band width. Below this sum the price leaves the active range inside one push interval and the LP carries the LVR.
The floor is expressed in minDispersionPbps PBPS by multiplying by the curve’s own scale, its dispRefPbps divided by its half-swing in bp (100 PBPS per bp for bell_100 and bell_500; keeper fence).
A second, weaker gate, , is also checked by the keeper fence, but it binds nothing once holds, because minFee alone already covers it.
The damped step function raises every emitted band to this floor, so a tightened gate can never stall the pool short of target: it is a floor on the step, not a test the step might fail.
6. Risk configuration
IPool.RiskConfig, written at listing, by the queued UPDATE_RISK op (executeUpdateRisk, TUNING, 1 hour), and instantly by setRiskConfigTighten on the one-way lane (§2). Four fields, one word:
| Field | Type | Units | Meaning | Range |
|---|---|---|---|---|
flags | uint16 | bitfield | feature, halt and depositor-gate bits | Inventory Management §10.1 |
kappaCovBps | uint16 | bps | convex coverage-wall strength | [50, 10000] on every listed asset (§3.1) |
depositCapCode | uint16 | m<<4|e | leg notional cap, m·10^e whole base tokens; checked after the liability write at deposit, donate and the swapLiability output leg (ExcessiveAmount), never on mark drift or hook yield. A queued raise is bounded at 10× per op | per leg |
maxLiabWeightBps | uint16 | bps | soft cap on the leg’s share of the pool claim book at credit time (WeightCapExceeded); 0 or 10,000 = off | off (Inventory Management §5.3) |
The instant lane accepts a write only when flags are identical except that DEPOSIT_GATED_BIT may be set, κ does not fall, the cap does not rise, and a live weight cap is neither cleared nor raised. Coverage reaches the quote through the fixed skew law (§4.1) and through (§6.2); neither takes anything else from this struct.
6.1. Flags
Full bit layout, masks and gating: Guards Overview §1. Two flags carry meaning beyond their name:
TOKEN_EXOTIC_BIT: the token rebases or taxes the sender on outflow (on-top fee-on-transfer). Set at listing:PoolIOLib.pushmeasures the pool’s balance delta and debits reserves for any excess;FLASH_ENABLED_BITand a yield hook are refused on the leg (InvalidConfig).DEPOSIT_GATED_BIT: depositor allowlist.depositanddonaterequireAccessControl.perms(msg.sender)to hold lane 9 (0x200, granted bysetPerms); exits andswapLiabilityare not gated. Arming is instant, clearing is queued.SWAP_GATED_BIT: swapper allowlist.swap_qein or out of the leg requirespermslane 10 (0x400); interior hops are not gated. Arming is instant, clearing is queued.
There is one halt lever, haltAsset(pool, token, src) / unhaltAsset(pool, token, src), and src selects which bit it moves: clearing one source never clears a halt another source set.
Common values: 0x0010 swaps enabled, 0x0030 swap + liability swap, 0x0070 swap + liability swap + flash (live values: Inventory Management §10.1). 0x0005 is the whole HALT_MASK (HALT_BIT and HALT_ANCHOR_BIT) and bit 1 is reserved, so 0x0007 is rejected as an unknown flag. RiskConfig cannot move a halt bit: listing ignores them and an update preserves the live ones (PoolConfigLib); only haltAsset, unhaltAsset and the guardian collapse touch HALT_MASK.
Feature bits gate their own entrypoints. deposit + cooldown + cross-asset withdrawTo reproduces a swap at the mark cap, fee for fee, so a cross exit takes LIABILITY_SWAP_ENABLED_BIT on both legs. A same-asset exit checks HALT_MASK only: a trading pause must never trap an LP’s exit on a depleted leg. Clearing SWAP_ENABLED_BIT alone leaves liability swaps and cross exits open; clearing both stops every trading path and leaves same-asset exits; haltAsset stops the leg, exits included. A leg listed with both bits clear refuses cross-asset exits from genesis.
6.2. Coverage wall
Coverage reaches the quote through two charges and never through the depth axis (traverse denominator: Inventory Management §4): the skew anchor (§4.1) and the wall below.
(kappaCovBps, bps) scales the convex coverage toll (Spread & Fees §6). Its practical meaning is the marginal rate at zero size (Spread & Fees §6.3), a level shift on which dust pays the same first-order rate:
Toll rate at zero size, for illustrative values (bps):
| 600 | 1,500 | 1,800 | 2,000 | 2,500 | |
|---|---|---|---|---|---|
| 0.99 | 6.1 bp | 15.2 bp | 18.2 bp | 20.2 bp | 25.3 bp |
| 0.98 | 12.2 | 30.6 | 36.7 | 40.8 | 51.0 |
| 0.96 | 25.0 | 62.5 | 75.0 | 83.3 | 104.2 |
| 0.90 | 66.7 | 166.7 | 200.0 | 222.2 | 277.8 |
| 0.80 | 150.0 | 375.0 | 450.0 | 500.0 | 625.0 |
| 0.50 | 600.0 | 1,500.0 | 1,800.0 | 2,000.0 | 2,500.0 |
| 0 | 0 | 0 | 0 | 0 |
How is sized, and why it is not sized on recovery. _covToll reads the drained
leg’s and nothing else, so an arbitrageur facing a mark error (bps) drains
until the marginal toll equals it, i.e. to the equilibrium coverage
Write for the fraction of that extraction the toll recovers. is a function of alone, cancels, and it rises monotonically to : , , , . The toll can never recover more than half the loss it prices, at any . A recovery target is therefore not a design input, and a low recovery ratio is not evidence the wall is under-sized. What sets is , and therefore the residual: net LP loss bps of . Sizing trades that residual against benign flow: the toll on a 1 %-of-leg trade starting at the peg must not exceed the leg’s own half-spread.
Sequencing. The marginal toll rises with , so raising it makes an already-under-covered leg more expensive to drain: refilling stays free, but it makes such a leg one-way until it is re-covered. Re-cover first, then raise.
Why κ has a floor. The buy-side traverse saturates, so the curve alone never stops a drain (Slippage & Price Impact §2.5). At κ = 600, for example, a leg at pays 150 bps of marginal toll against a spread of a few bps.
Coverage restoration takes no parameter, and neither does LP settlement: every exit settles at the pool rate , which nets a drained leg against a filled one, with no clock, no flag and no governance-set rate (Inventory Management §5). The one writer that can lower liquidityIndexWad is Pool.hookWriteDown, which realizes an actual loss on invested reserves, sized at and fenced to that leg.
7. Oracle configuration
IPool.OracleConfig, written at listing (AddAssetPayload.oracleCfg) and by UPDATE_ANCHOR. It names a lane, not an address: the chain has one mark store, in the Pool implementation, carrying a primary (P) and a reference (R) tier on every lane. There is no repoint op; the store moves only with the implementation at a fleet upgrade.
| Field | Type | Meaning |
|---|---|---|
lane | uint8 | store lane (< 64) carrying the leg’s P mark and its R reference; must be registered |
refBandBps | uint16 | symmetric tolerance of the P mark against the lane’s R tier (200 = 2%); mandatory non-zero on every spoke (PoolConfigLib.requireExternalSpokeBound) and capped at MAX_EXTERNAL_REF_BAND_BPS = 500; must be 0 on the base, which carries the parity halt instead |
mode | uint8 | 0 = EXTERNAL (the lane’s P mark), 1 = INTERNAL (cash-collateral peg) |
quoteUnit | uint8 | 0 = QUOTE_UNIT_ANCHOR (the norm), 1 = QUOTE_UNIT_UOA (unit-of-account bridge) |
Under QUOTE_UNIT_ANCHOR the feed is already attested in the asset’s own anchor’s units and the pool re-denominates nothing, which is the only dimensionally sound option past depth 1 (Anchor Path Pricing §6). QUOTE_UNIT_UOA is the unit-of-account bridge, for legs whose mark can only be signed as <TOKEN>-USD; the pool divides out the base’s own USD price at consumption, and it is legal only while the asset anchors directly to the base, enforced at config time.
mode selects where the mark comes from; the two modes are specified in Mark Relayer. The config-time bound is INTERNAL eligibility:
lanemust be registered.refBandBpsmust be armed.refBandBpsmust be at mostMAX_STABLE_DEPEG_BAND_BPS= 50, so a loosely or variably pegged unit (rebaser, FX, yield-bearing) cannot fit the band and is rejected.
There is no Asset.pegB64 field: the peg is a constant in the synthetic feed, not stored per asset.
Global oracle halts (FeedMathLib.gate), all fail-closed:
| Condition | Constant | Error |
|---|---|---|
| guardian fast-freeze bit set | FEED_HALT_BIT | FeatureDisabled(Err.Resource.FEED) |
age > ttl | per feed | StaleData |
| mark = 0 | - | ZeroValue |
confidence > 1000 bps | MAX_CONFIDENCE_HALT_BPS | ThresholdViolation |
| base mark off parity by more than its band (depeg code x 250 bps, 2,500 at code 0) | PoolConstantsLib.DEPEG_STEP_BPS | BaseDepegged |
Per-push acceptance band on the writer side, canonical statement: Mark Relayer. publishMarks accepts a segment only from an allowlisted relayer and only within 120 s of its source second. Kept facts: ; (6σ sanity cap), s (the off-chain mark pipeline’s Parkinson window), ; = stored prior (never the incoming push’s own, else a signer inflates its own band); stored floored at realized move, hence the cap (uncapped self-widens ~6x per max-band push).
8. Fee parameters
IPool.FeeParams, per pool, written by the timelocked UPDATE_FEES op (executeUpdateFees).
| Field | Type | Units | Meaning |
|---|---|---|---|
protoSharePct | uint8 | base 100 | protocol’s share of the fee pot |
flashFeePbps | uint16 | PBPS | flash-loan rate, MIN_FLASH_FEE_PBPS = 10 (0.001%) to MAX_FLASH_FEE_PBPS = 10,000 (1%) |
getFeeParams() returns the triple protoSharePct, flashFeePbps, flowCooldownSecs, and UPDATE_FEES writes the whole struct, so an op must restate every field it does not intend to move. |
There is no zero default to fall back on. Pool.initialize routes its FeeParams through the same PoolConfigLib.setFeeParams the admin lane uses, and that write floors flashFeePbps at MIN_FLASH_FEE_PBPS = 10 (0.001%) and flowCooldownSecs at MIN_FLOW_COOLDOWN_SECS = 1, so a pool handed a zero-initialised struct reverts at creation rather than launching with flash free.
The spread , the order the contract evaluates the toll and the fee in, and why nothing caps from above: Spread & Fees §3 and §5.1.
9. Pricing curves
Shared curves, one immutable SSTORE2 blob per curveId (NUQuarticLib.pack). Assets point in via Asset.curveId.
| Field | Type | Meaning | Constraint |
|---|---|---|---|
curveId | uint16 | id of the blob | 0 = no shape, refused at assign; there is no fallback quote |
wQ | int256[] | control weights, PBPS times Q | nondecreasing, non-flat |
interior | uint256[] | interior knots on the depth axis | strictly increasing in (0, 10000), 14 segments |
dispRefPbps | uint16 | reference dispersion of the fit (PBPS) | non-zero; quotes y-scale by dispersion/dispRefPbps |
flags | uint8 | opaque codebook key stamped by the fit | never read on chain; a curveId is written once (PoolConfigLib.setCurve) |
median | uint16 | derived, not an input: density median , the curve’s own zero | computed by NUQuarticLib.pack from the stored segments and packed into the header; _skewToDepth anchors zero skew on it |
Axes: x is cumulative depth = 0-100%, y is price offset from the mark, scaled linearly by dispersion/dispRefPbps. Basis: clamped quartic I-spline, monotone by , at every knot, exact range integration via stored prefix integrals.
Install validation (NUQuarticLib._validate):
- weights nondecreasing
wQ[last] > wQ[0](a flat curve carries no impact)- knots strictly increasing inside the open interval
- segments 14
dispRefPbps != 0
9.1. Installed presets
Shapes, weights and costs of every installed preset: Curve Presets. Preset ids are per deployment: pool composition.
9.2. Why the table exists: a quantized density codebook
An asset’s observed depth density is fitted off chain and then mapped to the nearest entry already in the table. Asset.curveId is a pointer, so re-pointing a leg at a different entry as its measured density drifts is cheap and frequent (UPDATE_PROFILE, one asset), while writing a curve is expensive and rare (UPDATE_CURVE; an installed id is immutable, so a refit is a new id the legs are repointed to). Splitting the two is what lets shaping track the tape without a governance action per leg.
The cost of the quantization is the residual between an asset’s own fit and its nearest entry. The continuous dispersion / dispRefPbps y-scale absorbs the scale part of that residual at quote time, so the table only has to span shape.
flags is an opaque off-chain codebook key: nothing on chain reads it, and the coverage wall is unconditional, so no leg lacks a wall for an entry to be mis-assigned to. Matching entry to leg is the fit’s job and the keeper pre-submit fence’s.
10. The fence set
Risk parameters carry no timelock on the steward path, so they can adapt. The one-way lane of §2 is unbounded in size because it is bounded in sign. The fences are the safety net, in three layers.
10.1. Layer 1: on-chain hard fences (IAdmin.RiskFences)
Owner-set, per (pool, token). setAssetParamsBounded fails closed when maxDeltaBps == 0, so an unseeded leg is not steward-writable. Eight uint16 fields, covering the parameters the steward lane can write:
| Fence | Meaning | Armed value (deploy scripts) |
|---|---|---|
minFeeHardMinPbps | absolute floor; also floors the owner path | the leg’s 2θ fee floor |
minFeeHardMaxPbps | absolute ceiling on a minFeePbps raise | a manifest multiple of the live minFeePbps |
vegaHardMinBps / vegaHardMaxBps | vega window | the live vegaBps / a manifest multiple of it |
maxDeltaBps | cumulative lowering envelope per 24 h window | a manifest word, per deployment |
maxCoopDiscBps | ceiling on coopDiscBps; binds every lane, owner included | 0 admits only a zero coop discount |
minCoopRebateBps / maxCoopRebateBps | rebate window; binds every lane, owner included | 0 admits only a zero rebate |
setRiskFences itself validates maxDeltaBps in , minFeeHardMinPbps non-zero and not above minFeeHardMaxPbps, vegaHardMinBps <= vegaHardMaxBps, and minCoopRebateBps <= maxCoopRebateBps <= BPS. Re-arming a fee, vega or maxDeltaBps fence deletes the leg’s steward window.
The deploy scripts arm the fences per leg after listing and re-arm after every TUNING op (Pools.armFences, Pools.rearmFences). Check Admin.riskFences(pool, token) rather than assuming: an unseeded leg is fail-closed rather than permissive, because setAssetParamsBounded refuses to run at all on maxDeltaBps == 0 (Invariants §12.1).
The fences bracket the shipped parameters; they are not targets. minFeeHardMinPbps sits below each leg’s live minFee: a fence at the leg’s own floor would leave no room to move down after a tape refit and would re-brick the keeper on any leg that has to. The invariant is enforced by the keeper’s startup gate and the coupling gates, not by minFeeHardMinPbps.
10.2. Layer 2: on-chain lowering envelope
A lowering of minFeePbps or vegaBps may not take the field below
where is the value at the start of the current 24 h window, ratcheted up by any raise inside it (AdminParamsLib._loosenOk, StewardWindow). The bound is on the displacement, not the step: N calls in one block compose to one window. A raise is exempt and is bounded by the hard fences alone. minLiquidity is not steward-writable at all: any change reverts.
10.3. Layer 3: off-chain coupling gates
Invariants the chain does not enforce, checked pre-submit so a violating plan is never broadcast:
| Gate | Rule | Why the chain cannot check it |
|---|---|---|
| theta coverage | (PBPS) | exists only in keeper config |
| halfwidth | minDispersionPbps ( + drift + ) times the cell scale | needs the measured drift distribution |
| vega headroom | minDispersionPbps + vegaHardMax·σ/1e4 <= dispersionCap(curve), σ per 1800 s bar = max(live sigmaPbps, keeper’s unfloored 14d σ) | the chain checks each vega write against the cap at the live print, never the fence’s max. σ is a typical level, not a stressed one: a pass leaves (cap − minDispersionPbps)·10⁴/vegaHardMax of σ headroom; a spike past it fails interior routes through the leg closed |
10.4. What is not fenced
| Parameter | Status | Bound |
|---|---|---|
minDispersionPbps | no steward fence | raise instant via raiseMinDispersion at the live curveId, cut queued on UPDATE_PROFILE; non-zero (0 → 1000 default), the keeper’s own halfwidth + vega-headroom floor, and a hard revert if it exceeds the curve’s dispersionCap (§5) |
| σ-driven κ ceiling | nothing to fence | clamped in _calculateDispersion at a protocol constant (§5); not a writable field |
curveId | no fence | timelocked (UPDATE_PROFILE) in both directions, a shape change being comparable in neither; the repoint re-checks curve existence and the leg’s dispersion floor against the new curve’s cap |
kappaCovBps | no fence | raise instant via setRiskConfigTighten, cut queued on UPDATE_RISK; [50, 10000] and hub-dominance (§3.1) |
minLiquidity | not steward-writable | owner lane only; any steward change reverts |
protoSharePct, flashFeePbps | no fence beyond MAX_FLASH_FEE_PBPS | timelocked |
anchor | no fence | timelocked at the GOVERNANCE base-migration tier, atomic with the oracle config (UPDATE_ANCHOR); guardian collapseAnchor may move a leg toward the root only, and halts it |
vegaBps spread across assets | no fence at all | each asset’s vega is fenced individually; nothing relates two assets’ values, and a large gap between two endpoints of a live route carries a bounded routing cost (§4.2.1) |
| the inventory skew | nothing to fence | it takes no parameter (§4.1) |
The fenced set is what the untimelocked steward lane can move; everything else already pays a timelock.
11. The fee floor covers 2 theta
11.1. Derivation
The pick-off bound , i.e. (PBPS), and why the floor and not the runtime spread must carry it: Toxic Flow Mitigation §2.2.
A QUOTE_UNIT_UOA leg’s mark divides out the base’s USD price, so its stale gap spans two push thresholds, the leg’s own and the base reference lane’s:
Example: a UOA leg at = 70 PBPS over a base lane at 45 needs 230; at 75, 240.
11.2. Enforcement
The predicate is minfee_covers_theta (Toxic Flow Mitigation §2.3), applied at four points:
- Startup, hard fail. At boot the oracle keeper reads
getAsset(token)for every configured pool times feed and refuses to start if any listed leg violates it. The keeper will not push into a pool it would make extractable. - Pre-submit. Any risk plan emitting a violating
minFeeis rejected before broadcast. - Parameter generation. Parameters are generated with
S_dep = max(q70, 2θ, feeQ99). - Deploy manifest. The listing ceremony refuses a row whose
minFeeis under , or under on a UOA leg.
The boundary is pinned by tests: minfee_covers_theta(50, 0.25) passes, (49, 0.25) fails, (1000, 5.0) passes.
Where a leg sits exactly at , any increase must be paired with a minFee increase in the same change, or the keeper hard-fails at its next restart.
12. Reference parameter table
Per-leg values (, , minFeePbps, minDispersionPbps, curveId) are deployment data: read getAsset(token) or see pool composition. The bounds every value must satisfy are in §3.1 and §6.
12.1. Topology configuration
The anchor column of a pool’s risk-param file is the sole expression of that pool’s topology: an empty column keeps every leg base-anchored, and filling a cell deepens that pool’s tree and puts the interior fence in the path of every route through it. What the depth bound is, what a deep edge requires and what gates the first fill are stated once at Anchor Path Pricing §1.1.
The parameter consequence is what belongs here: a non-base anchor gives that pair its own edge, its own feed, its own and its own fee floor, so several fee tiers coexist inside one pool instead of everything being priced off the base mark. Every sum-over-legs budget in this reference is sized against the full depth bound; nothing special-cases a shallower tree.
The interior fence’s second-order margin has no market tape until deep routes run; until then it is covered by tests.
13. Units reference
| Quantity | Base | Precision | Example |
|---|---|---|---|
Fees (minFeePbps, flashFeePbps) | 0.0001% | 100 = 1 bp, 10,000 = 1% | |
Volatility (sigmaPbps) | = 100% | 0.0001% | 10,000 = 1%; cap |
Dispersion (minDispersionPbps, κ) | 0.0001% | 1000 = 0.1%; ceiling 900,000 (MAX_DISPERSION_PBPS) | |
Coverage wall (kappaCovBps), confidence, refBandBps | 0.01% | 100 = 1% | |
| Multiplier () | 0.01% | 10,000 = 1.0x | |
Protocol share (protoSharePct) | 100 | 1% | 10 = 10% |
| Prices | WAD | 18 dp | 1e18 mark from the oracle |
Path spread (SwapQuote.spreadPbps) | 0.0001% | uint16, saturating at 65,535 = 6.55% |
13.1. Why PBPS
Standard BPS () cannot express a stablecoin fee below 1 bp:
| Fee | Standard | PBPS | In bp |
|---|---|---|---|
| 0.001% | inexpressible | 10 | 0.1 |
| 0.005% | inexpressible | 50 | 0.5 |
| 0.01% | 1 | 100 | 1 |
| 0.0001% | inexpressible | 1 | 0.01 (MIN_FEE_PBPS) |
Stable fee floors sit at fractions of a bp up to a few bp (read per-leg values from getAsset(token).minFeePbps). Standard BPS would round each to a whole bp (1.65 bp to 2, 0.9 bp to 1), and on a stable book a 0.5 bp against a 1 bp floor decides whether the pool wins the flow.
True 0.001 bp would need fractional PBPS. minFeePbps is integer PBPS, so 1 PBPS = 0.01 bp is the finest representable floor.
14. Known gaps
| Gap | Impact | Reference |
|---|---|---|
RiskFences can be left unseeded on a fresh deployment. Until they are seeded, maxDeltaBps == 0 and setAssetParamsBounded fails closed on every leg, so the risk keeper cannot act at all. | risk adaptation stays offline; parameters are frozen at their deploy values until an owner transaction seeds the fences | Admin.setAssetParamsBounded |
| Impact conservation is exact only at par. The traverse denominator is reserves, so it is while the skew step is denominated on , and no single pair of arm slopes conserves at every coverage. The residual is trader-pays (LP-safe): PBPS at 1% of depth, at 25%. | a round trip off par over-charges slightly; the LP is never the loser | Inventory Management §3.2. Full fix (traverse redenominated on ) scoped, not built |
vegaBps uniformity across assets has no fence at all. Each asset’s vega is fenced individually and nothing relates them, while _pathSpread prices the whole path’s at the endpoint maximum. The skew scales with , bounded only by the fence window. Either lane can introduce heterogeneity one asset at a time: the owner lane at once on a raise and after the TUNING delay on a cut, the steward lane instantly inside its fences. | bounded routing cost: Spread & Fees §11.2 | §4.2.1 |
| The interior fence is unexercised until deep routes run. With every leg base-anchored no route carries an interior leg. | the fence’s second-order margin has no tape until deep routes run; until then it is covered by tests only | §12.1 |
15. Related documentation
- Spread & Fees: the fee model and both theorems
- Toxic Flow Mitigation: the adverse-selection budget and the derivation
- Inventory Management: coverage and the pool rate
- Liquidity Shaping: the spline and the dispersion law
- Invariants: settlement and liquidity-floor invariants
- Price feed reads: mark, , confidence, push API