Pool Deployment & Curation

Any address can deploy an AIMM pool through PoolFactory. Administering one is a separate, gated matter: createPool takes no owner parameter, and every admin-gated call on every pool, deployer-created or official, resolves the same single, chain-wide AccessControl.owner(). Read §2 before assuming the deploying address holds keys.

Everything below is per chain. A pool, its PoolFactory, Admin and AccessControl all live on one chain, and each supported chain carries its own set. Today that is Arc testnet (chain id 5042002) only; other chains land as they deploy.


2. Pool Ownership & Control

2.1. Pool Deployment

// Deploys an ERC-1967 beacon proxy of the canonical Pool impl and runs `initdata` // atomically. There is NO owner parameter: the deployer does not become the administrator. address pool = factory.createPool( baseToken, // anchor token, e.g. USDC tokens, // address[] - factory discovery index + deployment salt. MUST be non-empty initdata // abi-encoded call forwarded to the pool, typically // initialize(baseToken, wnative, feeParams) );

createPool reverts ZeroAddr on a zero base and InvalidInput on an empty tokens array: there is no “deploy an empty pool” path. Registering a token in the discovery index is not listing it: assets become tradeable only through Admin (§3.1).

2.2. Ownership Model

There is no per-pool owner. “AC owner” below means the single AccessControl.owner(), identical across every pool on that chain.

Ownership cannot be transferred directly: transferOwnership and renounceOwnership both revert unconditionally. Rotation uses Solady’s two-step handover: requestOwnershipHandover by the incoming owner, completeOwnershipHandover by the sitting one.

ActionOpTierDelay
Halt / un-halt an assethaltAsset / unhaltAsset-none
Batch risk opbatchRiskOp-none
Collapse an anchor toward the rootcollapseAnchor-none (guardian; also halts the leg)
Set flow cooldownsetFlowCooldown-none
Set asset params, pre-seal or defensive tightensetAssetParams-none
Set asset params, any weakeningUPDATE_ASSET_PARAMSLOW1 hour
Add assetADD_ASSETLOW1 hour
Update risk configUPDATE_RISKLOW1 hour
Update profile (re-point presetId)UPDATE_PROFILELOW1 hour
Update a preset curveUPDATE_CURVELOW1 hour
Update fee paramsUPDATE_FEESLOW1 hour
Update oracle configUPDATE_ORACLEBASE2 days
Update the fee sinkUPDATE_TREASURYHIGH3 days
Install / replace an asset hookUPDATE_HOOKHIGH3 days
Re-anchor a legUPDATE_ANCHORCRITICAL7 days
Migrate the base tokenMIGRATE_BASE_TOKENCRITICAL7 days
Pool beacon impl swap (at PoolFactory)-UPGRADE7 days

Delays are the PROD_DELAYS schedule: LOW 1 hour, BASE 2 days, HIGH 3 days, CRITICAL 7 days, UPGRADE 7 days, ROTATION 7 days, FACTORY 14 days. Testnet deploys run the same tiers on an hours-scale schedule, so the timelock is exercised end to end rather than documented.

Every matured op has a 7-day grace window (GRACE_PERIOD). Left unexecuted past it, the op reverts Expired and must be re-requested. Shipping in the next release, that re-request needs no cancel first: a request on a key already past eta + GRACE_PERIOD overwrites the dead entry and takes a full fresh delay (Deployment & Upgrades §6.1).

A hook install rides the custody tier rather than the listing tier because a hook takes fund custody: it deploys reserves to an external venue. Re-anchoring rides the base-migration tier because it re-roots the subtree beneath the leg and re-denominates everything pricing through it.

Deposit, withdraw and swap are not owner-gated, but they are gated by the leg’s halt bits, which the owner and any guardian can set immediately. “Not owner-gated” is not “always available”.


3. Token Curation

3.1. Adding an Asset

Two phases, one generic queue entrypoint. subject is the left-padded token address.

admin.requestOp( pool, uint8(IPool.OpType.ADD_ASSET), bytes32(uint256(uint160(token))), abi.encode(IAdmin.AddAssetPayload({ oracleCfg: cfg, // IPool.OracleConfig, §3.2 riskCfg: risk, // IPool.RiskConfig, §3.3 presetId: presetId, // uint16 pointer into the pool's shared curve table minFeePbps: minFeePbps, // uint16, per-swap fee floor; 1 <= x <= 10_000 PBPS minDispersionPbps: minDisp, // uint32, dispersion floor; 0 => protocol default 1000 vegaBps: vegaBps // uint16, σ-sensitivity; 10_000 = 1.0x; MUST be >= 1, no default })) ); // After the LOW delay: admin.executeAddAsset(pool, token);

Note minDispersionPbps is uint32; the other three scalars are uint16.

Note what the payload deliberately does not carry:

  • The token itself. It is the subject and the executeAddAsset argument, so the two can never disagree.
  • initialPrice or vol-EMA seeds. Mark and σ live on the feed, not in pool storage.
  • decimals. It is read from the token and must land in 1..18.
  • The dispersion band’s ceiling. Neither an argument nor per-asset state: it derives from the leg’s preset curve via Pricing.dispersionCap, and minDispersionPbps is sanitized against it.
  • Inventory skew. Protocol law, identical on every leg of every pool.

Pre-seal bootstrap. Before a pool is opened to public liquidity, Admin.addAsset(pool, token, oracleCfg, riskCfg, presetId, minFeePbps, minDispersionPbps, vegaBps) lists an asset directly, with no timelock, and Admin.setCurve installs curves directly. Admin.sealBootstrap(pool) closes both paths permanently; afterwards every listing is the timelocked ADD_ASSET op above and every curve write is UPDATE_CURVE. Seal before the pool takes outside liquidity.

3.2. Oracle Configuration

Use mode = 0 (EXTERNAL): the leg’s mark is read from whatever IOracle you point primary at. mode = 1 (INTERNAL) pegs the mark at 1.0 and is legal only for cash-collateralized 1:1 tokens. Base cannot be INTERNAL.

The spoke depeg gate (refFeedId / refBandBps / a distinct refPrimary) is mandatory on every non-base leg in both modes. Base is exempt and carries the parity halt instead, at BASE_DEPEG_HALT_BPS = 500.

Field order matters. This is the on-chain declaration order, and positional abi.encode against a reordered copy produces silent garbage:

struct OracleConfig { bytes32 feedId; // mark feed id on `primary` address primary; // IOracle: mark source (EXTERNAL) or depeg gate (INTERNAL) uint8 mode; // 0 = EXTERNAL (recommended), 1 = INTERNAL (cash-collateral peg only) uint8 quoteUnit; // 0 = ANCHOR (the norm), 1 = UNIT_OF_ACCOUNT bridge uint16 refBandBps; // symmetric tolerance, BPS (200 = ±2%); 0 = disabled, refused on a spoke bytes32 refFeedId; // reference feed for the depeg band address refPrimary; // oracle serving refFeedId; MUST differ from `primary` while armed }

quoteUnit = 1 bridges <TOKEN>-USD marks by dividing out the base’s own USD price. It is legal only while the leg’s anchor is the base token: the correction divides by the base price, so it is meaningless for a deeper anchor, and the config validator rejects it there.

Writing an IOracle adapter

primary can be any contract returning FeedData. An adapter that only reads chain state has zero off-chain dependency:

interface IOracle { struct FeedData { uint256 mark1e18; // mark, WAD. The quote source uint32 sigmaPbps; // σ in PBPS, pricing input uint32 updatedAtSecs; // push timestamp uint16 ttlSecs; // freshness window, seconds uint16 confidenceBps; // 1σ CI in BPS. Spread surcharge + halt gate uint16 flags; // bit0 = paused (fail-closed in the gate and in isFeedFresh) uint16 maxDeviationBps; // per-push max mark move, BPS. MUST be non-zero uint48 sourceTsMs; // source time, ms since epoch. Monotonic replay guard } function getFeed(bytes32 feedId) external view returns (FeedData memory); function isFeedFresh(bytes32 feedId, uint32 maxAge) external view returns (bool); function isFeedFresh(bytes32 feedId) external view returns (bool); } contract ChainlinkOracleAdapter is IOracle { AggregatorV3Interface public immutable agg; constructor(AggregatorV3Interface agg_) { agg = agg_; } function getFeed(bytes32) external view returns (FeedData memory d) { (, int256 answer,, uint256 updatedAt,) = agg.latestRoundData(); d.mark1e18 = uint256(answer) * 1e10; // 8-decimal feed → WAD d.sigmaPbps = 0; // supply a real σ if you have one d.updatedAtSecs = uint32(updatedAt); d.ttlSecs = 3600; d.confidenceBps = 0; d.flags = 0; d.maxDeviationBps = 500; // MUST be non-zero d.sourceTsMs = uint48(updatedAt) * 1000; } function isFeedFresh(bytes32 id, uint32 maxAge) public view returns (bool) { (,,, uint256 updatedAt,) = agg.latestRoundData(); return block.timestamp - updatedAt <= maxAge; } function isFeedFresh(bytes32 id) external view returns (bool) { return isFeedFresh(id, 3600); } }

maxDeviationBps is mandatory and non-zero: the pool quotes off the raw pushed mark, so an unbounded push is a single-transaction drain. Anything reading chain state, a Uniswap TWAP, a vault NAV, another pool’s mid, fits the same interface. See Oracles.

3.3. Risk Configuration

struct RiskConfig { uint16 flags; // feature + halt bits uint16 kappaCovBps; // convex coverage-wall strength }

Two fields, κ the only number. Everything else a curator might expect to tune is protocol law:

  • Inventory skew is fixed in code, and saturates at c <= 0.5 and c >= 2.0.
  • The price-impact denominator is the leg’s own raw reserves with a zero-guard.
  • An under-covered leg heals through the exit haircut when an LP leaves rather than on a schedule.

Bit layout for flags: Basic Operations §2.

κ coupling. Every listed asset, including the hub, must carry kappaCovBps > 0. A non-zero κ forces haircutSuppressorBps == 0 on the same leg (a wall plus a live haircut suppression is a toll-exempt coverage-declining path), and κ can never afterwards be stripped from a leg whose preset carries FLAG_REQUIRES_WALL. Canonical statement: Invariants I-9.

3.4. Per-Asset Params

setAssetParams writes four fields on a listed leg. It applies immediately before sealBootstrap, or when the write is a defensive tighten (minLiquidity and haircutSuppressorBps unchanged, minFeePbps and vegaBps non-decreasing). Any other write queues at the LOW tier and lands via executeSetAssetParams.

admin.setAssetParams( pool, token, minLiquidity, // uint128, hard outflow floor in token units, bounded < 2**96 minFeePbps, // uint16, 1 <= x <= 10_000 PBPS (1%) vegaBps, // uint16, σ-sensitivity slope; 10_000 = 1.0x haircutSuppressorBps // uint16, MUST be < 20_000, and == 0 when kappaCovBps > 0 );

minFeePbps floors the leg’s contribution to the path spread. It is the only fee rate a leg carries and there is no ceiling parameter: the σ, confidence and staleness terms widen the spread without an upper bound. Traders bound their own execution with minAmountOut; see Basic Operations §7.

setAssetParamsBounded is the risk-steward twin: same four fields, clamped by per-asset RiskFences plus a relative risk-up delta, with a defensive tighten exempt from the relative clamp.

struct RiskFences { uint16 minFeeHardMinPbps; uint16 minFeeHardMaxPbps; uint16 vegaHardMinBps; uint16 vegaHardMaxBps; uint16 haircutSuppressorHardMaxBps; uint16 haircutSuppressorHardMinBps; uint16 maxDeltaBps; // 0 = fences unset, the bounded path reverts }

Both haircut bounds are present deliberately. maxDeltaBps bounds step size, not cumulative displacement, so N calls in one block compose into an unbounded ratchet; the absolute Hard{Min,Max} pairs are what actually fence each parameter. minLiquidity is fenced by being immovable on the bounded path rather than by a pair of bounds.

3.5. Pricing Curve

Price impact is set by preset. Curves are clamped quartic I-splines, C2 density, monotone by construction, held in a shared per-pool table; each asset points in via presetId.

admin.setCurve( pool, presetId, // uint16 interior, // uint256[] strictly-increasing depth-axis knots wQ, // int256[] nondecreasing control weights (Δw >= 0 ⇔ monotone) dispRefPbps, // uint16 reference dispersion of the fit flags // uint8; bit 0 = FLAG_REQUIRES_WALL );

Pre-seal this writes directly; post-seal it is the timelocked UPDATE_CURVE op. Quotes scale by dispersion / dispRefPbps.

A new preset, live on an asset, is one hour, not two. Installing a shape and pointing a leg at it are two ops, and they are keyed differently: UPDATE_CURVE by presetId, UPDATE_PROFILE by token. Different keys means they occupy different queue slots, so both can be requested at once and mature together rather than in series. UPDATE_PROFILE validates that the curve exists at execute time, not at request time, so a profile queued against a preset that does not exist yet is fine as long as the curve op lands first.

// t = 0: queue both. Different keys, so neither displaces the other. admin.requestOp(pool, uint8(IPool.OpType.UPDATE_CURVE), bytes32(uint256(NEW_PRESET)), abi.encode(interior, wQ, dispRefPbps, flags)); admin.requestOp(pool, uint8(IPool.OpType.UPDATE_PROFILE), bytes32(uint256(uint160(token))), abi.encode(NEW_PRESET, minDispersionPbps)); // t = 1 hour: curve FIRST, then the repoint. Same block is fine. admin.executeSetCurve(pool, NEW_PRESET); // the shape now exists admin.executeUpdateProfile(pool, token); // validates it, then binds

Order matters only at execute: reverse it and the profile op reverts because the preset it names is not installed, costing the request window and nothing else.

A curve carries at most 14 segments (NUQuartic.MAX_SEGS, enforced on every write): wQ.length - 4 <= 14, and interior.length must equal wQ.length - 5. A flat curve, wQ[last] == wQ[0], is rejected as no price discovery.

The table is a quantized density codebook: a leg’s observed depth density is fitted off chain and mapped to the nearest entry, and presetId is only a pointer. Re-pointing a leg as its density drifts is the cheap, frequent operation; writing a curve is the expensive, rare one. The continuous dispersion / dispRefPbps y-scale absorbs the scale part of the residual, so the codebook spans shape only.

The preset id space is defined below; which ids are actually installed is per chain. All sit on interior knots [1314, 8686]:

presetIdHalf-swing at dispRefPbpsdispRefPbpsflagsAssignable to
1100 pbps (1 bp)100 pbps0any leg
2100 pbps (1 bp)100 pbpsFLAG_REQUIRES_WALLkappaCovBps > 0 only
3200 pbps (2 bp)100 pbpsFLAG_REQUIRES_WALLkappaCovBps > 0 only
4500 pbps (5 bp)100 pbpsFLAG_REQUIRES_WALLkappaCovBps > 0 only
5500 pbps (5 bp)500 pbps0any leg

Per-chain assignment:

ChainInstalledIdsScope
Arc testnet (5042002)4 presets1, 2, 4, 5; preset 3 is not installed26 symbols, 37 legs across 4 pools

A symbol is not a leg: a token listed in more than one pool is one symbol and several (pool, token) legs, and each leg carries its own presetId, κ and params.

Do not assume an id resolves on a given chain. Read the pool’s curve table, or the chain’s risk-params record, before pointing a leg at one.

Half-swing is the offset the quote reaches at the edge of the depth axis when the leg’s dispersion equals dispRefPbps. A narrow half-swing paired with a coverage wall gives low slippage near mid and leaves the edge to the wall; a wide one gives flatter slippage across a broader range. Asymmetric flow is handled by the Avellaneda-Stoikov inventory skew (Liquidity Shaping §5), not by a directional preset shape.

Never synthesize one preset by rescaling another; re-fit. The three wQ vectors are independent fits and differ from exact multiples by 1–2 ULP. The residual is far below quote granularity, but the shipped vectors are canonical on-chain state and a rescaled regeneration fails parity against them.

3.6. Halting and Removal

admin.haltAsset(pool, token, HALT_RISK_BIT); // guardian or owner admin.unhaltAsset(pool, token, HALT_RISK_BIT); // owner only

Halt sources refcount: unhaltAsset clears only the bit you name, so lifting a guardian halt never relists a leg an owner risk halt still holds down.

Permanent removal is not supported. Halt the asset, let LPs withdraw over time, migrate to a new pool if needed.


4. Custom Hooks

Per-asset void callbacks (preOutflow, optional postInflow) plus a dual ledger. Yield and rehypothecation are strategy-side; there is no fee-override surface. Admin path: requestOp(UPDATE_HOOK)executeSetAssetHook (HIGH tier), or clearAssetHook (immediate, requires invested == 0). Full surface: Hooks.


5. Singleton Contracts

A small set of standalone singletons serves every pool on a chain. There is no Diamond or module-registry pattern; Pool proxies do use fixed-target DELEGATECALLs internally, which is not per-pool-configurable module trust.

ContractRole
PoolAMM: swap, deposit, withdraw, withdrawTo, donate, swapLiability. Mark via IOracle or the INTERNAL peg
AdminTimelocked configuration, per pool. UUPS behind an ERC-1967 proxy
FlashERC-3156-style flash loans. UUPS behind an ERC-1967 proxy
PoolFactoryDeploys ERC-1967 beacon proxies. It is the beacon: fleet implementation + impl-swap timelock

Each holds an immutable reference to that chain’s shared AccessControl.

Extending the protocol. Two supported forms: external contracts calling Pool directly, and per-asset hooks. New behaviour for every pool means deploying a new Pool implementation and queueing a 7-day beacon swap at PoolFactory; on execution every live pool on that chain picks it up, not just future ones.

5.1. Factory Reference

// Discovery: read-only, anyone getPoolTokens(pool) // tokens registered in a pool getPoolsForToken(token) // all pools containing a token getCommonPools(tokenA, tokenB) // official pools containing both - routing candidates getOfficialPoolsForToken(token) // official pools containing a token isPool(addr) // was this deployed by this factory isOfficialPool(addr) getAllPoolsCount() / getOfficialPoolsCount() officialPools(uint256) // indexed getter; enumerate against the count poolBaseTokens(pool) beacon() // returns the factory itself implementation() // the fleet implementation slot // Fleet upgrade: AC owner, 7-day UPGRADE tier requestReferenceUpgrade(newImpl) / executeReferenceUpgrade() / cancelReferenceUpgrade() // AC owner, immediate, no timelock deregisterPool(pool) // evicts a pool from every discovery index setProtocolDeployer(newDeployer) // the address whose pools enter the official index // Called BY a pool, isPool-gated: not callable by integrators registerTokens(tokens) / setPoolBaseToken(newBase)

deregisterPool and setProtocolDeployer are untimelocked owner writes. deregisterPool is de-pollution: it deletes the pool from allPools, officialPools, poolToTokens, tokenToPools and isPool, so a deregistered pool stops appearing in route views and emergency sweeps while remaining fully live on chain. setProtocolDeployer re-points the address whose future createPool calls land in the official index, in one transaction with no notice.

There is no allPools() and no referencePool(). The full pool list is an internal array; enumerate with getAllPoolsCount() plus officialPools(i), or read /v1/pools.

getCommonPools and getOfficialPoolsForToken read the official index only, which a pool joins when its creator is the factory’s protocol deployer. That address is internal and not readable on chain, so treat official membership as something you query (isOfficialPool) rather than predict.

5.2. Admin Reference

// Queue + lifecycle requestOp(pool, opType, subject, payload) cancelTimelock(pool, opType, subject) // owner or guardian // Execute, one per op executeAddAsset(pool, token) executeSetAssetParams(pool, token) executeUpdateRiskConfig(pool, token) executeUpdateProfile(pool, token) executeSetCurve(pool, presetId) executeUpdateFeeParams(pool) executeTreasuryUpdate(pool) executeOracleUpdate(pool, token) executeAnchorUpdate(pool, token) executeSetAssetHook(pool, token) executeBaseMigration(pool, spokes) // Immediate addAsset(...) / setCurve(...) / sealBootstrap(pool) // pre-seal bootstrap only setAssetParams(...) / setAssetParamsBounded(...) / setRiskFences(...) haltAsset / unhaltAsset / batchRiskOp / collapseAnchor setFlowCooldown(pool, cooldownSecs) // 0..300 s, default 15 s clearAssetHook(pool, token) // requires invested == 0 collectProtocolFees(pool, token) // caller must be pool.treasury()

6. Worked Example

A stablecoin pool, base USDC, adding USDT as a spoke.

// 1. Deploy. No owner param: authority resolves to the AC owner regardless. address pool = factory.createPool( USDC, tokens, // non-empty abi.encodeCall(IPool.initialize, (USDC, WETH, FeeParams(25, 5))) ); // 25% protocol fee share; flashFeePbps = 5 // 2. Queue the spoke. USDC is the base here: exempt from the ref-band mandate, // carries the parity depeg halt instead, and must run mode = EXTERNAL, quoteUnit = 0. admin.requestOp( pool, uint8(IPool.OpType.ADD_ASSET), bytes32(uint256(uint160(USDT))), abi.encode(IAdmin.AddAssetPayload({ oracleCfg: IPool.OracleConfig({ feedId: keccak256(abi.encodePacked(USDT, USDC)), primary: externalOracle, mode: 0, // EXTERNAL quoteUnit: 0, // ANCHOR refBandBps: 50, // <= MAX_STABLE_DEPEG_BAND_BPS (50) refFeedId: keccak256(abi.encodePacked(USDT, USD)), // mandatory on every spoke refPrimary: refOracle // MUST differ from `primary` }), riskCfg: IPool.RiskConfig({ flags: SWAP_ENABLED_BIT, kappaCovBps: 100 // > 0 required; preset 2 is wall-gated }), presetId: 2, minFeePbps: 100, // 1 bp minDispersionPbps: 100, // uint32 vegaBps: 10000 // 1.0x })) ); // 3. Wait the LOW delay (1 hour, every chain), then land it. admin.executeAddAsset(pool, USDT); // 4. Per-asset params. Pre-seal or a defensive tighten applies immediately; // a weakening queues at LOW and lands via executeSetAssetParams. admin.setAssetParams(pool, USDT, minLiquidity, 100, 10000, 0);

7. Checklist

7.1. Deployment sequence

  1. Base token + spoke list, tokens non-empty at createPool
  2. Curve presets installed (setCurve) before the first listing that references them
  3. Feeds: mark + mandatory spoke ref* and band; mode = EXTERNAL unless the leg is a cash-collateral 1:1 token (§3.2)
  4. kappaCovBps > 0 on every listed leg, haircutSuppressorBps == 0 alongside it (§3.3)
  5. Per-asset params: minLiquidity, minFeePbps, vegaBps, haircutSuppressorBps (§3.4)
  6. sealBootstrap(pool) before public liquidity; after this every listing is timelocked (§3.1)

Deployment is permissionless. Listing and oracle writes are not.

The rest of this section is the curation checklist: the decisions that are yours to get wrong, and the ones that are not yours at all.

7.2. Before you deploy

  • Accept the fleet beacon. An executed executeReferenceUpgrade re-points every live pool in one transaction, yours included (§5). Your only lever during the window is cancelReferenceUpgrade, and only if you hold owner or guardian. _validateImplementation pins the new implementation’s AC, admin and flash; it does not check storage-layout compatibility, and nothing on-chain does. That check is a review obligation; PoolStorage’s append-only rule is enforced at build time, not at execute time.
  • Confirm the chain before every governance send. The pool salt includes block.chainid, so a right call on the wrong chain is a different pool.

7.3. Keys and principals

  • Put the AC owner on a multisig with a signer set larger than the guardian set. The only rotation path is the two-step handover (§2.2), which rejects a zero target and, once the quorum policy is armed, requires the incoming owner to satisfy ceil(2n/3) itself. You cannot fat-finger the owner away and you cannot burn it either: plan for a key that must stay live forever. (Access Control & Roles §1.1)
  • Size the guardian set knowing the residual. A guardian can veto every queued governance op and can revokeSigner the oracle below quorum. Both fail closed, so a hostile guardian can halt the protocol indefinitely and cannot be timed out. Guardians never un-halt, widen or write params.
  • Keep treasuryOwner separate from owner. They are independent principals by design; collapsing them into one key discards the custody split.
  • Set RiskFences before granting a risk steward. With maxDeltaBps == 0 the fences are unset and setAssetParamsBounded reverts (§3.4). A steward with no fences is not a constrained steward, it is a broken one.

7.4. Feeds, before the asset is listed

  • Register the feed on the live oracle. On ExternalOracleV4 that is registerFeed(feedId, globalIndex, expBias, maxDeviationBps, ttlSecs), AC-owner only. It rejects feedId == 0, globalIndex > 255, maxDeviationBps == 0, maxDeviationBps > MAX_DEV_THRESHOLD (2,000 bps), ttlSecs == 0, an expBias outside [-16, 96], a feedId already registered (FeedAlreadyExists) and a globalIndex already taken. There is no σ seed and no sourceTsMs stamp: σ and confidence arrive in the pushed blob, and registration seeds a fresh slot’s clock so the first push into it must be strictly newer.
  • Pick maxDeviationBps for the pair’s real move size, because every instant lever ratchets it down: updateFeed(feedId, maxDeviationBps, ttlSecs) is guardian-or-AC-owner and rejects any value above the stored one on both fields, so it tightens or holds. Loosening is the AC-owner’s timelocked requestFeedWidenBASE delay → executeFeedWiden, shipping in the next release and guardian-vetoable throughout; until it ships, a band set too tight for the pair is escaped only by deploying a new oracle and re-pointing every leg that reads it with a BASE-tier UPDATE_ORACLE. (Oracles §8.3)
  • Set expBias to the feed’s own octave centre and check it after listing. expHeadroom(feedId) returns (stepsUp, stepsDown); on a live lane the pair sums to 15, a healthy feed reads near (8, 7), and (0, 0) means no live price at all. A feed pinned at (0, 15) or (15, 0) drops out of the blob on the next move in that direction.
  • Pick ttlSecs from the feed’s real cadence, not from comfort. Past TTL every read reverts StaleData: swaps, deposits and withdrawals alike. A TTL shorter than the keeper’s worst observed lag is a scheduled outage.
  • Verify the spoke depeg band is genuinely independent. Address inequality between primary and refPrimary (§3.2) is the on-chain floor only; signer and admin disjointness between the two oracles is a deploy-time obligation nothing on-chain checks. (Depeg Halt §2.4)
  • Treat registerFeed as terminal. There is no removeFeed, a feedId once registered reverts FeedAlreadyExists, and its globalIndex is taken for good. A wrong binding is fixed by a new feed id at a free index, not an edit.

7.5. Base and topology

  • Choose the base for peg quality, not for volume. Beyond the parity band every swap in the pool reverts BaseDepegged until the peg returns or a CRITICAL-tier re-point lands. (Depeg Halt §2.2)
  • Keep the anchor column empty unless you need depth. Every filled cell is a CRITICAL-tier UPDATE_ANCHOR, and an anchored child inherits its parent’s depeg risk with no automatic breaker: the only lever on a parent depeg is the guardian haltAsset or collapseAnchor. (Depeg Halt §2.5)

7.6. Risk parameters and hooks

  • Set minLiquidity deliberately. Any change queues at LOW in either direction, and a raise halts outflow the moment it lands, so the instant lane is closed to it on both edges.
  • Leave flowCooldownSecs non-zero. 0 disables the JIT guard entirely, and swap and flash fees move the liquidity index, so a zero cooldown lets a bot deposit ahead of a known-inbound swap and withdraw the fee. (Flow Guards)
  • Note what does not exist: there is no per-leg spread ceiling (§3.4) and no pool-wide pause bit. Halt granularity is per-asset only, and the two-sided 1 <= minFeePbps <= 10_000 PBPS bound is the whole defense against an admin key pinning a punitive floor in one write.
  • Vet a hook’s venue as if it held the capital outright, because it does. hookWriteDown cuts Rinv, R and the LP index. A venue loss is socialised to that leg’s LPs, not to the protocol.
  • Know that the hook’s setters are untimelocked, and who holds them. setMaxHarvestCreditBps, setBuffer, setMerklDistributor and forceWriteDown resolve AccessControl(AC).owner(), the same principal that owns the pool rather than one outside the tree, and setIncentivesReceiver resolves AccessControl(AC).treasuryOwner(). None of them queue, so the AC owner can write down your LPs’ index in one transaction with no notice, on a lever that carries none of the timelock the equivalent pool write does.
  • Set maxHarvestCreditBps (default 100, ceiling MAX_HARVEST_CREDIT_BPS = 500) and read it as a rate, not an allowance: at most B·κ·Δt/(104·86400) for a book B and cap κ in bps, so a second harvest in the same block credits nothing. 0 disables credit entirely. The pool clamps independently at MAX_HOOK_CREDIT_BPS_PER_DAY = 500: it clamps, it does not revert, so a mis-set hook silently under-credits rather than failing loudly.

7.7. Operations

  • Rehearse the halt path before you need it, and enumerate your legs via getOfficialPoolsForTokengetPoolTokens ahead of time; a sweep you compose during an incident is a sweep you compose slowly. Know what it costs your users: checkRiskFlags gates deposit, donate, withdrawTo and swapLiability on HALT_MASK, not just swaps, so halting a leg shuts the exit as well as the entrance. Say so in your own comms before you use it. Authority per lever: Access Control & Roles §4.
  • Monitor the gates that fail closed, with alerts on the revert rather than on a dashboard: feed age against ttlSecs, confidenceBps against the halt threshold, base deviation against the parity band, each spoke’s |p-pref| against refBandBps, and Rliq-minLiquidity per leg. Every one of these turns a live leg into a reverting leg with no intermediate degraded state. Surfaces: Observability.
  • Have a plan for a leg you want gone. Permanent removal does not exist (§3.6) and a halt blocks the exit, so the order is: stop the inflow, wait for LPs to leave, then halt.