Pool Deployment & Curation
Deploying an AIMM pool is owner-gated: PoolFactory.createPool is onlyAdmin, which resolves to the chain’s single AccessControl.owner(). Every pool shares one CREATE3 namespace (the factory is the CREATE3 sender), so any other caller could spend a mined reservation on a pool of its own. Administering a pool is a separate seat, named by the owner inside initdata as poolAdmin_ (§7.8). A seat of 0 is the protocol sentinel and resolves to that same AccessControl.owner(), which is what every protocol pool carries. Read §2 before assuming the seat holder holds protocol keys.
Everything below is per chain. A pool, its PoolFactory, Admin and AccessControl all live on one chain. See deployed instances for which chains carry a set.
The protocol’s own fleet is not deployed this way. It ships from one manifest per chain, <slug>.manifest.json, through three phases in order: Protocol (both oracle tiers, AccessControl, Admin and Flash UUPS proxies, the Pool implementation, PoolFactory, guardians, depositors, treasury pin), Pools (one pool per roster: create, list every leg, sealBootstrap, seed, brand OFFICIAL, queue the treasury op) and Assets (one leg onto a live pool, queue and execute as separate signatures because a real timelock sits between them). Each phase has a dry run that broadcasts nothing. Each writes a record stamped with manifestHash, the keccak of the exact manifest bytes that produced it. The runbook is DEPLOY.md.
2. Pool Ownership & Control
2.1. Pool Deployment
// AC owner only. Mints a `PoolProxy` (ERC-1967) of the canonical Pool impl through the
// CREATE3 factory and runs `initdata` atomically. No owner parameter: the administering
// seat is named in `initdata` as `poolAdmin_` (`0` = protocol sentinel). See §7.8.
address predicted = factory.predictPool(salt); // the address, before anything is deployed
address pool = factory.createPool(
salt, // bytes32, the mined reservation
baseToken, // anchor token, e.g. USDC
tokens, // address[] - factory discovery index. MUST be non-empty
initdata // abi-encoded call forwarded to the pool, typically
// initialize(baseToken, wnative, feeParams, poolAdmin, lpTag)
); // pool == predictedcreatePool reverts ZeroAddress on a zero base token and InvalidInput on an empty tokens array: there is no empty-pool path. Registering a token in the discovery index is not listing it; assets become tradeable only through Admin (§3.1).
The address comes from the CREATE3 factory (0x815d9297bB4dFf9eCaEB7C53017E0DE40300C75e, one address on every chain it is deployed to, Contract Addresses) with the PoolFactory as sender, so the effective salt is keccak256(poolFactory ++ salt) and the address is a function of those three values only. Consequences: the same salt lands on the same pool address on every chain of a fleet; the deploy key is not in the address, unlike the singletons; and the base token and token roster are mutable pool state rather than address inputs, so a base migration or a new listing never moves a pool. predictPool(salt) returns the address from chain data alone.
2.2. Ownership Model
“AC owner” below means the single AccessControl.owner(), identical across every pool on that chain. It is what a sentinel pool’s seat resolves to, and every table below that names it assumes one.
Ownership cannot be transferred directly: transferOwnership and renounceOwnership revert. The rotation path depends on whether the quorum policy is armed: Admin §1.
Instant (no timelock):
| Action | Op |
|---|---|
| Halt / un-halt an asset | haltAsset / unhaltAsset |
| Batch risk op | batchRiskOp |
| Collapse an anchor toward the root (guardian; also halts the leg) | collapseAnchor |
| Set flow cooldown | setFlowCooldown |
| Set asset params, pre-seal or defensive tighten | requestAssetParams |
| Tighten risk config, raise κ, switch swap / liability-swap / flash off | setRiskConfigTighten |
Every other op queues through Admin.requestOp or the factory upgrade at a GOVERNANCE, LISTING or TUNING tier (7 days, 1 day, 1 hour under PROD_DELAYS); the op-to-tier map is Access Control §4 and the duration table Access Control §3. There is no oracle repoint op: a leg’s OracleConfig names a store lane, written at listing and by UPDATE_ANCHOR.
Every matured op has a 7-day grace window (GRACE_PERIOD_SECS). Left unexecuted past it the op reverts Expired and must be re-requested, with no cancel first: a request on a key already past eta + GRACE_PERIOD_SECS overwrites the dead entry and takes a full fresh delay (Admin).
The split is structure versus magnitude: a week for anything that changes code, custody of protocol money or the pool’s root; a day for what is listed and how it prices, hook custody included; an hour for bounded numbers. De-risking writes have no delay. Full authority matrix: Access Control, Timelock.
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 does not mean 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
curveId: curveId, // 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
lpSym: lpSym // bytes32, receipt SYM: 1..8 chars [A-Za-z0-9], NUL-padded, unique per pool
}))
);
// After the LISTING delay:
admin.executeAddAsset(pool, token);minDispersionPbps is uint32; the other three scalars are uint16. lpSym is last and sets the LP receipt symbol: 1..8 ASCII alphanumerics, right-padded with NUL, unique within the pool, e.g. bytes32("USDT").
What the payload does not carry, and why:
| Absent | Reason |
|---|---|
| the token | It is the subject and the executeAddAsset argument, so the two can never disagree |
initialPrice, vol-EMA seeds | Mark and σ live on the feed, not in pool storage |
decimals | Read from the token, must land in 1..18 |
the receipt symbol from token.symbol() | Listed, not read: lpSym carries it, so a token with a long, empty or non-alphanumeric symbol() still lists. Wraps keep the W (WBNB) |
| the dispersion ceiling | Derives from the leg’s curve via PricingLib.dispersionCap; minDispersionPbps is sanitized against it |
| inventory skew | Fixed in code, identical on every leg of every pool |
Pre-seal bootstrap: Admin.addAsset(pool, token, oracleCfg, riskCfg, curveId, minFeePbps, minDispersionPbps, vegaBps, lpSym) 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 the primary (P) tier of the store lane the config names. mode = 1 (INTERNAL) pegs the mark at 1.0 and is legal only for cash-collateralized 1:1 tokens. Base cannot be INTERNAL. Model and gates: Mark Relayer, Glossary: Oracle.
The config names a lane, not an address: each chain has one mark store, inside the Pool implementation, and every lane carries a primary (P) and a reference (R) tier signed by separate rosters. The spoke depeg band (refBandBps, the tolerance of the P mark against the lane’s R tier) is mandatory on every non-base leg in both modes and capped at MAX_EXTERNAL_REF_BAND_BPS = 500. Base is exempt and carries a parity halt instead: its band is the depeg code in RiskConfig.flags bits 12 to 15 times 250 bps, 2,500 bps at code 0, never wider.
Field order is the on-chain declaration order. A positional abi.encode against a reordered copy produces a wrong config with no revert:
struct OracleConfig {
uint8 lane; // store lane (< 64), registered; carries the leg's P and R tiers
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 of P vs R, BPS (200 = ±2%); 0 on the base, refused on a spoke
}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.
There is no adapter surface. A leg cannot point at a contract of its own: marks enter only through the store’s publishMarks, signed by the tier’s k-of-n roster, and the pool reads them as a word per lane. Third-party contracts read the same marks through PoolFactory.getFeed / feedOf (Consuming Price Feeds). Roster rotation and the per-lane class table (maxDevBps, ttlSecs, minSigmaPbps) are store-level, not per-leg: Mark Relayer.
3.3. Risk Configuration
struct RiskConfig {
uint16 flags; // feature + halt bits; bit 9 = DEPOSIT_GATED_BIT
uint16 kappaCovBps; // convex coverage-wall strength, 50..10_000
uint16 depositCapCode; // m<<4|e: m·10^e whole base tokens of leg notional; m == 0 refused
uint16 maxLiabWeightBps; // soft cap on the leg's share of the pool claim book; 0 = off
}Two of the four fields are guarded-launch caps, both soft: they bind after the liability write at deposit, donate and the swapLiability output leg, and a leg’s notional drifts with its mark afterwards. depositCapCode reverts ExcessiveAmount past m·10^e base tokens; maxLiabWeightBps reverts WeightCapExceeded past the leg’s share of the claim book. DEPOSIT_GATED_BIT in flags makes deposit and donate require AccessControl.perms(msg.sender) lane 9 (0x200); exits and swapLiability are never gated.
Every write goes through the direction rule: setRiskConfigTighten lands now when every field tightens (κ up, cap down, an ENABLE bit cleared, a GATE_MASK bit (9 DEPOSIT_GATED, 10 SWAP_GATED) set, weight cap kept or lowered), everything else is a queued UPDATE_RISK with a cap raise bounded at 10× per op.
Three things a curator cannot tune: 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; LP settlement is the pool-level rate C rather than a per-leg haircut, so an under-covered leg is priced by the skew and the wall and its LPs are paid min(c_leg, C) in kind or C in another token (Inventory Management).
Bit layout for flags: Basic Operations §2. TOKEN_EXOTIC_BIT (bit 8) declares a rebasing or on-top fee-on-transfer token at listing: the pool measures and debits its actual outflow on every push, and flash and a yield hook are refused on the leg. A fee carved out of the transfer is paid by the recipient and is invisible to the pool, so the quote and minAmountOut are checked on face and that floor is the recipient’s to enforce on its own balance. No official pool lists a rebasing or fee-on-transfer token.
κ coupling: every listed asset including the hub must carry MIN_KAPPA_COV_BPS = 50 <= kappaCovBps <= SC.BPS = 10,000. Every κ writer calls requireNeverDepletable, which reverts InvalidInput outside that range so the floor c* = kappa/(kappa+BPS) sits in [0.5%, 50%], and requireHubKappaDominant, so a spoke’s κ never exceeds its hub’s. κ can never afterwards be stripped from any leg: the floor holds on every writer. Canonical statement: Invariants I-9.
3.4. Per-Asset Params
requestAssetParams writes five fields on a listed leg. It applies immediately before sealBootstrap, or when the write is a defensive tighten (minLiquidity unchanged, minFeePbps and vegaBps non-decreasing, coopDiscBps non-increasing, coopRebateBps non-decreasing). Any other write queues at the TUNING tier and lands via executeUpdateAssetParams, which compares each field against its request-time snapshot and reverts AssetParamMoved if a field the op moves changed underneath.
admin.requestAssetParams(
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, >= 1
coopDiscBps, // uint16, BPS of the LP fee discounted on a coop fill, <= 3_000
coopRebateBps // uint16, BPS of the coop caller's gain returned to LPs, <= 10_000
);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 (Basic Operations §7).
setAssetParamsBounded is the risk-steward twin: same arguments, inside per-asset RiskFences, never queues. Clamp rules: Risk Steward §2.
struct RiskFences {
uint16 minFeeHardMinPbps;
uint16 minFeeHardMaxPbps;
uint16 vegaHardMinBps;
uint16 vegaHardMaxBps;
uint16 maxDeltaBps; // 0 = fences unset, the bounded path reverts
uint16 maxCoopDiscBps;
uint16 minCoopRebateBps;
uint16 maxCoopRebateBps;
}minLiquidity is fenced by being immovable on the bounded path rather than by a pair of bounds. The coop fences bind every lane, owner included; all zero admits only zero coop terms.
3.5. Pricing Curve
Price impact is set by curve. Curves are clamped quartic I-splines, C2 density, monotone, held in a shared per-pool table; each asset points in via curveId (Liquidity Shaping).
admin.setCurve(
pool,
curveId, // 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; opaque off-chain codebook key, immutable across refits
);Pre-seal this writes directly; post-seal it is the timelocked UPDATE_CURVE op. Quotes scale by dispersion / dispRefPbps.
A new curve live on an asset is one day, not one day plus an hour. Installing a shape (UPDATE_CURVE, LISTING) and pointing a leg at it (UPDATE_PROFILE, TUNING) are keyed differently, by curveId and by token, so they occupy different queue slots and both can be requested at once. UPDATE_PROFILE validates that the curve exists at execute time, not at request time.
// t = 0: queue both. Different keys, so neither displaces the other.
admin.requestOp(pool, uint8(IPool.OpType.UPDATE_CURVE),
bytes32(uint256(NEW_CURVE)), abi.encode(interior, wQ, dispRefPbps, flags));
admin.requestOp(pool, uint8(IPool.OpType.UPDATE_PROFILE),
bytes32(uint256(uint160(token))), abi.encode(NEW_CURVE, minDispersionPbps));
// t = 1 day: curve FIRST, then the repoint. Same block is fine.
admin.executeUpdateCurve(pool, NEW_CURVE); // the shape now exists
admin.executeUpdateProfile(pool, token); // validates it, then bindsOrder matters only at execute: reverse it and the profile op reverts because the curve it names is not installed; the cost is one request window.
A curve carries at most 14 segments (NUQuarticLib.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: it gives 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 curveId 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 (Parametrization §9.2).
Installed ids are per chain; each preset’s numbers and fit guidance are on Curve Presets. Read the pool’s curve table before pointing a leg at an id. A symbol is not a leg: a token listed in more than one pool is one symbol and several (pool, token) legs, each with its own curveId, κ and params.
flags is an off-chain codebook key; the chain never reads it, so it constrains no assignment. Its one on-chain property is that a refit of a live curveId must pass the same byte.
Never synthesize one curve by rescaling another; re-fit. Distinct wQ presets are independent fits and differ from exact multiples by 1–2 ULP. The residual sits far below quote granularity, but the installed vectors are canonical on-chain state and a rescaled regeneration fails parity against them.
3.6. Halting and Removal
admin.haltAsset(pool, token, HALT_BIT); // guardian, owner or seat
admin.unhaltAsset(pool, token, HALT_BIT); // owner; the seat once the guardian notice has runsrc is HALT_BIT, or COOP_ENABLED_BIT for a one-way coop kill that leaves the leg live. A guardian or owner halt stamps the release clock: the seat clears it only after LISTING from that stamp, the owner at any time.
Permanent removal is not supported. Halt the asset, let LPs withdraw over time, migrate to a new pool if needed.
4. Custom Hooks
One void callback (preOutflow, the liquidate on a shortfall; investing is keeper-only) plus a dual ledger. Yield and rehypothecation are strategy-side; there is no fee-override surface. Admin path: requestOp(UPDATE_HOOK) → executeUpdateHook (LISTING tier, 1 day), or clearAssetHook (immediate, liquidates the invested tranche first). A hook is a clone of one implementation per venue kind (Aave V3, ERC-4626, Compound V2, Morpho Blue), one per (pool, leg), with AC, pool, token and the venue’s addresses as immutable args. The deployer creates the clone and anyone may run its one-shot initialize(), which only reads those args and the venue. The hook starts with maxInvested 0, so it invests nothing until the owner lands a curve with a cap. 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.
| Contract | Role |
|---|---|
Pool | AMM: swap_qe, deposit, withdraw, withdrawTo, donate, swapLiability. Marks from its mark store lanes or the INTERNAL peg |
Admin | Timelocked configuration, per pool. UUPS behind an ERC-1967 proxy |
Flash | ERC-3156-style flash loans. UUPS behind an ERC-1967 proxy |
PoolFactory | Deploys PoolProxy contracts and alone may upgrade them: fleet implementation + impl-swap timelock |
Each holds an immutable reference to that chain’s shared AccessControl.
Two supported extension 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 fleet upgrade at PoolFactory; on execution every live pool on that chain moves to it, not just future ones (Admin).
5.1. Factory Reference
// Discovery: read-only, anyone
getPoolTokens(pool) // tokens registered in a pool
getPoolsForToken(token) // all pools containing a token
getOfficialPoolsForToken(token) // official pools containing a token
isPool(addr) // was this deployed by this factory, still listed
isClone(addr) // was this deployed by this factory, ever (survives deregisterPool)
isOfficialPool(addr)
poolAuthority(pool) // (clone, listed, official, admin, treasury, sealed, poolAdmin) in one read
getAllPoolsCount() / getOfficialPoolsCount()
officialPools(uint256) // indexed getter; enumerate against the count
poolBaseTokens(pool)
implementation() // the fleet implementation slot
predictPool(salt) // the address a salt deploys to, before it is deployed
CREATE3_FACTORY() // the CREATE3 factory every pool is minted through
STORAGE_VERSION() // genesis storage layout version; an upgrade may only move it forward
pendingReferencePool() // implementation queued by requestReferenceUpgrade; 0 = none
upgradeTimelock() // eta (unix seconds) of the queued fleet upgrade
// Feeds: the third-party read path
getFeed(feedId) / feedOf(feedId, tier) // FeedData of the lane's P tier (tier 1) or R tier (tier 2)
laneOf(feedId) // store lane + 1; 0 = unmapped
// AC owner, immediate
createPool(salt, baseToken, tokens, initdata) // §2.1
// Fleet upgrade: AC owner, GOVERNANCE tier, 7 days
requestReferenceUpgrade(newImpl) / executeReferenceUpgrade() / cancelReferenceUpgrade()
// AC owner, immediate: map a third-party feedId to a registered store lane
setFeedLane(feedId, lane) // emits FeedLaneUpdated
// Official brand: owner grants, owner-or-guardian revokes/vetoes. Pre-arm (quorum policy
// not yet armed) a grant lands instantly; post-arm it queues at LISTING tier.
requestOfficial(pool, official) / executeOfficial(pool) / cancelOfficial(pool)
pendingOfficial(pool) // ETA of a queued grant; 0 = none
syncOfficial(pool) // permissionless: strips the brand once the pool's fee sink left the protocol treasury
// AC owner, immediate, no timelock
deregisterPool(pool) // evicts a pool from every discovery index
// Called BY a pool, isClone-gated: not callable by integrators
registerTokens(tokens) / setPoolBaseToken(newBase)deregisterPool is an untimelocked owner write and index cleanup only: it clears isPool and the pool’s rows in tokenToPools / officialTokenToPools, so a deregistered pool stops appearing in route views and emergency sweeps while remaining fully live on chain and keeping its isClone credential (it can still list assets or migrate its base).
Feed reads are covered in Consuming Price Feeds. There is no allPools() and no referencePool(). Enumerate with getAllPoolsCount() plus officialPools(i), or read /v1/pools.
Official brand is a grant, never derived from the creator. requestOfficial(pool, true) is owner-only; while the quorum policy is unarmed it lands the same transaction, once armed it queues an ETA at the LISTING tier and executeOfficial re-asserts the pool’s shape (still isClone, still listed, still sealed) before landing it. Revoke (requestOfficial(pool, false)) is always instant and owner-or-guardian. getOfficialPoolsForToken reads that index.
5.2. Admin Reference
// Queue + lifecycle
requestOp(pool, opType, subject, payload)
cancelOp(pool, opType, subject) // owner or guardian on a pool with no seat, only the seat on a seated pool
// Execute, one per op
executeAddAsset(pool, token)
executeUpdateAssetParams(pool, token)
executeUpdateRisk(pool, token)
executeUpdateProfile(pool, token)
executeUpdateCurve(pool, curveId)
executeUpdateFees(pool)
executeUpdateTreasury(pool)
executeUpdateAnchor(pool, token)
executeUpdateHook(pool, token)
executeMigrateBaseToken(pool, spokes)
executeSweep(pool, token) // pool emits Swept
executeBackfillLegs(pool, legs) // payload = abi.encode(legs); pool emits LegsBackfilled
// Immediate
addAsset(...) / setCurve(...) / sealBootstrap(pool) // pre-seal bootstrap only
requestAssetParams(...) / setAssetParamsBounded(...) / setRiskFences(...)
setRiskConfigTighten(pool, token, cfg) // tightening writes only
raiseMinDispersion(pool, token, floor) // raise-only; steward or owner
narrowRefBand(pool, token, refBandBps) // narrow-only; steward or owner
haltAsset / unhaltAsset / batchRiskOp / collapseAnchor
acceptPoolAdmin(pool) // the named next seat, §7.8
setFlowCooldown(pool, cooldownSecs) // raise-only, 1..300 s, default 15 s
clearAssetHook(pool, token) // sync, recall, drop (recall-only on a remainder)
claimTreasury(pool, token) // caller must be pool.treasury()
// Admin's own UUPS upgrade: requestUpgrade(newImpl) / executeUpgrade() / cancelUpgrade()Module surface: Admin.
6. Worked Example
A stablecoin pool, base USDC, adding USDT as a spoke.
// 1. Deploy, AC owner only. `createPool` has no owner param; the administering seat is
// named inside initdata as `poolAdmin_`. `predictPool(salt)` gives the address first.
address pool = factory.createPool(
salt, // bytes32, the mined reservation
USDC,
tokens, // non-empty
abi.encodeCall(Pool.initialize, (USDC, WETH, FeeParams(25, 100, 15), poolAdmin, lpTag))
); // 25% proto share; flashFeePbps 100 (0.01%);
// flowCooldownSecs 15; poolAdmin = the seat;
// lpTag = bytes32 receipt tag (code letter + name)
// 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({
lane: USDT_LANE, // registered store lane, < 64
mode: 0, // EXTERNAL
quoteUnit: 0, // ANCHOR
refBandBps: 50 // mandatory on every spoke, <= 500
}),
riskCfg: IPool.RiskConfig({
flags: SWAP_ENABLED_BIT,
kappaCovBps: 100, // >= MIN_KAPPA_COV_BPS (50); the wall is unconditional
depositCapCode: (5 << 4) | 5, // 5·10^5 = 500k base tokens of USDT notional
maxLiabWeightBps: 0 // weight cap off
}),
curveId: 2,
minFeePbps: 100, // 1 bp
minDispersionPbps: 100, // uint32
vegaBps: 10000, // 1.0x
lpSym: bytes32("USDT") // receipt symbol B{code}-USDT
}))
);
// 3. Wait the LISTING delay (1 day in production), then land it.
admin.executeAddAsset(pool, USDT);
// 4. Per-asset params. Pre-seal or a defensive tighten applies immediately;
// a weakening queues at TUNING and lands via executeUpdateAssetParams.
admin.requestAssetParams(pool, USDT, minLiquidity, 100, 10000, 0, 0); // no coop terms7. Checklist
7.1. Deployment sequence
- A reserved
salt, base token + spoke list,tokensnon-empty atcreatePool - Curves installed (
setCurve) before the first listing that references them - Lanes: a registered store lane per leg and a mandatory spoke
refBandBps;mode = EXTERNALunless the leg is a cash-collateral 1:1 token (§3.2) RiskConfigper leg:kappaCovBpsin[50, 10000]and never above the hub’s, a realdepositCapCode,DEPOSIT_GATED_BITset if the launch is allowlisted (§3.3)- Per-asset params:
minLiquidity,minFeePbps,vegaBps(§3.4) sealBootstrap(pool)before public liquidity; after this every listing is timelocked (§3.1)
Deployment is owner-gated, as are listing and oracle writes. Trading, LPing and flash stay permissionless.
7.2. Before you deploy
- Accept the fleet upgrade. An executed
executeReferenceUpgradecallsupgradeToon every pool proxy in one transaction, yours included (§5). Your only lever during the window iscancelReferenceUpgrade, and only if you hold owner or guardian._validateImplementationpins the new implementation’sAC,adminandflash; 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.
block.chainidis not in a pool salt, so a fleet’s pools carry the same address on every chain: a correct call on the wrong chain hits a real pool with real liquidity, not a dead address.
7.3. Keys and principals
- Put the AC owner on a multisig with a signer set larger than the guardian set. After arming, the only rotation is
queueRole(OWNER)→GOVERNANCE→executeRole, which re-checks the incoming Safe against the admin quorum policy (Admin §1). The owner cannot be lost to a wrong address and cannot be burned; plan for a key that stays live indefinitely (Access Control). - Size the guardian set knowing the residual. A guardian can veto every queued governance op, a mark-store roster rotation included, and can halt any lane instantly. 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.
- Know that the fee sink is the pool’s own
treasury(), seeded fromAccessControl.treasury()atinitializeand moved by anUPDATE_TREASURYop on theGOVERNANCEtier. There is no separate treasury principal: whoever holds the pool’s seat can redirect fee custody after that delay (Protocol Fee Collection). - Set
RiskFencesbefore granting a risk steward. WithmaxDeltaBps == 0the fences are unset andsetAssetParamsBoundedreverts (§3.4). - Size
depositCapCodefor the launch, not for the leg’s target size. It is checked at every credit against the leg’s notional at the live mark, so a capped leg whose own mark is unusable refuses every deposit, and a raise past 10× the live cap takes more than oneUPDATE_RISK. Grant depositorsAccessControl.setPerms(who, 0x200, true)before settingDEPOSIT_GATED_BIT: arming is instant and un-gating is queued.
7.4. Lanes, before the asset is listed
- Use a lane both of whose tiers are live. A listing validates the lane is registered; it does not check freshness. Every armed spoke band fails closed once the lane’s R tier passes its TTL, so the reference tier is relayed on the same discipline as the primary (Mark Relayer).
- Know that the lane’s class (
maxDevBps,ttlSecs,minSigmaPbps) is fixed in the store: there is no per-feed register, update or widen call, and no per-leg repoint. Pick the lane whose class fits the pair’s real move size and cadence; past TTL every read revertsStaleData: swaps, deposits and withdrawals alike (Heartbeat). - Do not reuse a lane within one anchor chain. Two nodes on one lane move together on a push, so the chain is refused at listing.
- Verify the spoke depeg band is independent. A distinct signing domain per tier is the on-chain floor only; roster disjointness between the two tiers is a deploy-time obligation nothing on chain checks (Flow Guards).
7.5. Base and topology
- Choose the base for peg quality, not for volume. Beyond the parity band every swap in the pool reverts
BaseDepeggeduntil the peg returns or aGOVERNANCE-tier base migration lands (Flow Guards). - Keep the anchor column empty unless you need depth. Every filled cell is a
GOVERNANCE-tierUPDATE_ANCHOR, and an anchored child inherits its parent’s depeg risk with no automatic breaker: the only lever on a parent depeg is the guardianhaltAssetorcollapseAnchor(Anchor tree, Flow Guards).
7.6. Risk parameters and hooks
- Set
minLiquiditycarefully. Any change queues at TUNING in either direction, and a raise halts outflow the moment it lands, so the instant lane is closed on both edges. - Keep
flowCooldownSecsat its default or higher. It is the JIT guard, and swap and flash fees move the liquidity index, so a short cooldown lets a bot deposit ahead of a known-inbound swap and withdraw the fee.setFlowCooldowntakes 1 to 300 s and only raises it; lowering is aUPDATE_FEESop (Flow Guards). - Know what does not exist: 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_000PBPS bound is the only defense against an admin key pinning a punitive floor in one write. - Vet a hook’s venue as a custodian: it holds the capital.
hookWriteDowncuts , and the LP index. A venue loss is socialised to that leg’s LPs, not to the protocol. - Know who holds the hook’s setters and which direction queues.
requestHarvestCreditCap,requestCurveandrequestWriteDownresolve the pool’s seat (Pool.owner(): the address named ininitdataon a foreign pool,AccessControl.owner()on a protocol pool), not a principal outside the tree. De-risking (lowering the credit cap, a curve with no point of L lower, no wider band and no highermaxInvested, a write-down within the already-measured shortfall) lands instantly; risk-increasing moves queue behind their own hook-local delay (tuneDelay()for the credit cap,riskUpDelay()for curve and write-down, both read off the AC schedule) and land via the matchingexecute*. The Merkl distributor is a constant with no setter. Incentives always route to the pool’s own 7d-updatabletreasury(). - Size
maxInvestedper leg, and treat it as the first cap, not the only one: the position is also held to 5% of the venue’s live cash, and no invest lands above 95% venue utilization. A venue that borrows out its cash after the invest can leave the position above 5%; nothing trims it except a liquidate. Pick venues by depth, not by APY. - Set
maxHarvestCreditBps(default 100, ceilingMAX_HARVEST_CREDIT_BPS = 500) and read it as a rate, not an allowance: at most for a book and cap in bps, so a second harvest in the same block credits nothing.0disables credit entirely. The pool clamps independently atMAX_HOOK_CREDIT_BPS_PER_DAY = 500: it clamps rather than reverting, so a mis-set hook silently under-credits.
7.7. Operations
- Rehearse the halt path before you need it, and enumerate your legs via
getOfficialPoolsForToken→getPoolTokensahead of time. Know what it costs your users:checkRiskFlagsgatesdeposit,donate,withdrawToandswapLiabilityonHALT_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. - Monitor the gates that fail closed, with alerts on the revert rather than on a dashboard: feed age against
ttlSecs,confidenceBpsagainst the halt threshold, base deviation against the parity band, each spoke’s againstrefBandBps, and per leg. Each turns a live leg into a reverting leg with no intermediate degraded state. Surfaces: Observability. - Plan the removal of a leg in advance. 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.
7.8. Self-named pool administration
Every pool carries its own seat (Pool.poolAdmin()). The protocol owner deploys the pool and names that seat inside initdata, so a curator gets its seat at creation and does not call the factory itself; later changes of hands use the handover below. A seat of 0 is the protocol sentinel and resolves to the live AccessControl.owner(), and every protocol pool is a sentinel pool.
- You administer your own pool. All config, queue, cancel and seal levers belong to the seat (
Pool.owner()). Delegate to a hot key by making the seat a Safe or a role module, not by handing over a private key. - Handover is 7d + accept.
requestOp(pool, UPDATE_POOL_ADMIN, 0, abi.encode(next))queues at the GOVERNANCE tier;acceptPoolAdmin(pool)by the named address moves the seat. The incumbent maycancelOpat any time. Staging0donates the pool back and only the live owner can accept it; staging the live owner is normalised to0. A handover is refused while the pool is official. - The protocol keeps two powers over a foreign pool. The guardian may raise
HALT_BITon any leg, and because a halt also gates the exit endpoints your LPs cannot leave that leg until it is cleared: the seat releases it after the LISTING notice period (1d production), measured from the first unreleased guardian halt, the owner may clear it immediately, and the guardian never clears it. Separately, the protocol may upgrade the sharedPoolimplementation, (which carries the mark store) for the whole fleet behind a 7d GOVERNANCE timelock, with no per-pool opt-out. Donating the pool back to the sentinel is the supported way to leave protocol governance. - No public deposits until you seal. A foreign pool that has not been
sealBootstrap-ed refuses publicdepositanddonate; the seat holder may seed its own book first. Once sealed, the public gate opens and every later listing uses the timelockedADD_ASSETlane. - Your LP receipts are neutral. Foreign pools mint
LP: <SYMBOL>/LP-<SYMBOL>;BTR <Name> Pool/B<code>-is reserved for protocol pools. - Re-check every
DELAY_*reference above against Access Control in your release; the production schedule is a deploy argument, not a constant in the pool.