---
title: "Pool Deployment & Curation"
description: "Deploying a pool, the chain-wide ownership model, and the curation surface: assets, oracles, risk, curves, halts"
audience: tech
type: explanation
status: live
lang: en
updated: "2026-09-04"
publish: true
---
# 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

```solidity
// 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.

| Action | Op | Tier | Delay |
|---|---|---|---|
| Halt / un-halt an asset | `haltAsset` / `unhaltAsset` | - | none |
| Batch risk op | `batchRiskOp` | - | none |
| Collapse an anchor toward the root | `collapseAnchor` | - | none (guardian; also halts the leg) |
| Set flow cooldown | `setFlowCooldown` | - | none |
| Set asset params, pre-seal or defensive tighten | `setAssetParams` | - | none |
| Set asset params, any weakening | `UPDATE_ASSET_PARAMS` | LOW | 1 hour |
| Add asset | `ADD_ASSET` | LOW | 1 hour |
| Update risk config | `UPDATE_RISK` | LOW | 1 hour |
| Update profile (re-point `presetId`) | `UPDATE_PROFILE` | LOW | 1 hour |
| Update a preset curve | `UPDATE_CURVE` | LOW | 1 hour |
| Update fee params | `UPDATE_FEES` | LOW | 1 hour |
| **Update oracle config** | `UPDATE_ORACLE` | **BASE** | **2 days** |
| Update the fee sink | `UPDATE_TREASURY` | HIGH | 3 days |
| Install / replace an asset hook | `UPDATE_HOOK` | HIGH | 3 days |
| Re-anchor a leg | `UPDATE_ANCHOR` | CRITICAL | 7 days |
| Migrate the base token | `MIGRATE_BASE_TOKEN` | CRITICAL | 7 days |
| Pool beacon impl swap (at `PoolFactory`) | - | UPGRADE | 7 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](/docs/3-2-deployment-upgrades#61-timelock-mechanics)).

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.

```solidity
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:

```solidity
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:

```solidity
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](/docs/3-4-oracles).

### 3.3. Risk Configuration

```solidity
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](/docs/5-1-1-basic-operations#2-check-the-flags-first).

**κ 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](/docs/1-1-8-invariants#i-9-coverage-toll-is-charge-only-and-terminal-only).

### 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`.

```solidity
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](/docs/5-1-1-basic-operations#7-slippage-protection).

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

```solidity
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`.

```solidity
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.

```solidity
// 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]`:

| `presetId` | Half-swing at `dispRefPbps` | `dispRefPbps` | `flags` | Assignable to |
|---|---|---|---|---|
| 1 | 100 pbps (1 bp) | 100 pbps | 0 | any leg |
| 2 | 100 pbps (1 bp) | 100 pbps | `FLAG_REQUIRES_WALL` | `kappaCovBps > 0` only |
| 3 | 200 pbps (2 bp) | 100 pbps | `FLAG_REQUIRES_WALL` | `kappaCovBps > 0` only |
| 4 | 500 pbps (5 bp) | 100 pbps | `FLAG_REQUIRES_WALL` | `kappaCovBps > 0` only |
| 5 | 500 pbps (5 bp) | 500 pbps | 0 | any leg |

**Per-chain assignment:**

| Chain | Installed | Ids | Scope |
|---|---|---|---|
| Arc testnet (5042002) | 4 presets | 1, 2, 4, 5; **preset 3 is not installed** | 26 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](/docs/1-1-2-liquidity-shaping)), 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

```solidity
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](/docs/5-1-3-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 `DELEGATECALL`s internally, which is not per-pool-configurable module trust.

| Contract | Role |
|---|---|
| `Pool` | AMM: `swap`, `deposit`, `withdraw`, `withdrawTo`, `donate`, `swapLiability`. Mark via `IOracle` 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 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

```solidity
// 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

```solidity
// 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.

```solidity
// 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](/docs/3-1-access-control-roles-emergency-powers#11-arming-the-quorum-policy))
- [ ] 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 `requestFeedWiden` → `BASE` 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](/docs/3-4-oracles#83-deviation-bounds))
- [ ] 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](/docs/3-5-depeg-halt#24-per-asset-price-band-depeg-guard-for-spokes))
- [ ] 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](/docs/3-5-depeg-halt#22-halt-threshold))
- [ ] 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](/docs/3-5-depeg-halt#25-coverage-at-depth-configuration-versus-capability))

### 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](/docs/3-3-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 $R_{inv}$, $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 \cdot \kappa \cdot \Delta t / (10^4 \cdot 86400)$ for a book $B$ and cap $\kappa$ 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 `getOfficialPoolsForToken` → `getPoolTokens` 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](/docs/3-1-access-control-roles-emergency-powers#4-halt-authority).
- [ ] 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 $\lvert p - p_{ref}\rvert$ against `refBandBps`, and $R_{liq} - \texttt{minLiquidity}$ per leg. Every one of these turns a live leg into a reverting leg with no intermediate degraded state. Surfaces: [Observability](/docs/3-8-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.

---

## 8. Related

- [Basic Operations](/docs/5-1-1-basic-operations) · [Cookbook §5](/docs/5-2-3-cookbook#5-deploy-a-pool) · [Quotes & Routing](/docs/5-2-2-quotes-routing)
- [Hooks](/docs/5-1-3-hooks) · [White Labeling](/docs/5-3-1-white-labeling)
- [Admin](/docs/1-2-3-admin) · [Oracles](/docs/3-4-oracles) · [Glossary: Oracle](/docs/glossary#oracle)
