Admin Contract

Admin is the single governance entry point for every AIMM pool: listing an asset, re-pointing a curve, changing risk or oracle config, moving the fee sink, re-anchoring the tree and halting a leg all route through it. Sensitive writes are two-phase (request, wait out a tier delay, execute inside a grace window); halts land immediately. This page lists each operation with its payload, tier, validation and events.


1. Overview

Admin (Admin.sol) is a standalone singleton serving every pool, so each public function takes address pool as its first arg. Owner authority routes through the shared AccessControl singleton. It is a UUPS implementation behind an ERC-1967 proxy, not a runtime-pluggable module: Pool bakes the proxy address as an immutable and PoolFactory._validateImplementation pins it on every fleet upgrade. Timelock state is keyed by (pool, opId[, subject]) locally in Admin, not in PoolStorage. Its calls into Pool (pool.adminHaltAsset(token, src)) are standard external calls that Pool answers directly; Pool then DELEGATECALLs the linked PoolConfigLib library for the state mutation, a compile-time-fixed target (§13).

Sensitive changes are timelocked two-phase (request → wait → execute, §4) with a grace period that expires stale ops (§3). Emergency halt bypasses the delay (§2).


2. Emergency Functions (No Timelock)

2.1. Halt / Unhalt Asset

src is HALT_BIT (bit 0) or, as a one-way kill, COOP_ENABLED_BIT: a coop kill clears that enable bit, voids a queued coop arm and starts no release clock. A guardian or owner HALT_BIT halt stamps guardianHaltAt, the seat’s release clock. collapseAnchor sets the separate HALT_ANCHOR_BIT (bit 2), which only a later UPDATE_ANCHOR clears.

function haltAsset(address pool, address token, uint16 src) external // guardian, owner or seat function unhaltAsset(address pool, address token, uint16 src) external // owner; seat after LISTING

Effects:

  • Sets or clears the given bit in the leg’s risk-config flags
  • Any bit in HALT_MASK blocks swaps, deposits, withdrawals, flash loans and interior-hop transit
  • Immediate, no timelock

On a pool without a seat, halting is guardian-or-owner and un-halting owner-only. A seated pool adds the seat on both edges, its clear held for LISTING after a guardian or owner halt: Access Control §2.

Use cases: security incident response, oracle failure, suspected exploit. Runbook: Guardian.

2.2. Batch Risk Ops

function batchRiskOp(address[] calldata pools, address[] calldata tokens, BatchOp op, uint16 src) external

BatchOp is {Halt, Unhalt}. Batch halt/unhalt across (pool, token) pairs in one tx, from an EOA or a multisig, no Safe MultiSend needed, carrying the same authority asymmetry: guardians get the halt edge only. Per-leg try/catch skips and logs a bad leg (BatchLegSkipped), so one failure never bricks an emergency sweep. A guardian-bit batch halt stamps each leg’s guardian-halt clock and a batch unhalt clears it, exactly as the single-leg calls do, so a later seat measures its release notice from the real halt. There is still no atomic pool-wide pause bit: protocol-wide halts enumerate assets off-chain and batch.


3. Timelock Delays

Every queued op sits in one of three tiers, GOVERNANCE (7 days), LISTING (1 day) or TUNING (1 hour). The op-to-tier map and payloads are the table in §4.1; tier rationale and the full duration table: Access Control. No module or ownership timelocks live in Admin: ownership sits on the shared AccessControl singleton.

Delays are deploy-time data, not code. Grace period: 7 days after the timelock matures; an operation not executed inside that window expires.


4. Two-Phase Execution Pattern

4.1. Step 1: Request

One generic entrypoint queues every op. There are no per-op request* functions.

function requestOp(address pool, uint8 opType, bytes32 subject, bytes calldata payload) external
  • opType is IPool.OpType, cast to uint8. The ordinal is the wire value: read it off the enum, never off a prose list. IPool.sol declares it grouped by timelock tier:

    OrdinalOpTypeOrdinalOpType
    0NONE7UPDATE_FEES
    1MIGRATE_BASE_TOKEN8UPDATE_PROFILE
    2UPDATE_ANCHOR9UPDATE_CURVE
    3UPDATE_TREASURY10UPDATE_ASSET_PARAMS
    4UPDATE_HOOK11SWEEP
    5ADD_ASSET12BACKFILL_LEGS
    6UPDATE_RISK13UPDATE_POOL_ADMIN

    NONE and UPDATE_ASSET_PARAMS are not requestable and revert; UPDATE_ASSET_PARAMS is queued only by requestAssetParams itself. Encoding a literal off a stale ordering is a live mis-routing hazard: a caller that sends 2 intending UPDATE_TREASURY queues UPDATE_ANCHOR.

  • subject is the third key component: bytes32(uint256(uint160(token))) for token-keyed ops, bytes32(uint256(curveId)) for UPDATE_CURVE, keccak256(abi.encode(legs)) for BACKFILL_LEGS, ignored by the four pool-wide ops (MIGRATE_BASE_TOKEN, UPDATE_TREASURY, UPDATE_FEES, UPDATE_POOL_ADMIN).

  • payload is abi.encode of the op’s arguments without the subject. Structs live in IAdmin.sol.

opTypetierkeypayload
UPDATE_RISKTUNINGkeccak256(pool, "UPDATE_RISK", token)IPool.RiskConfig
UPDATE_PROFILETUNINGkeccak256(pool, "UPDATE_PROFILE", token)(uint16 curveId, uint32 minDispersionPbps)
UPDATE_ASSET_PARAMSTUNINGkeccak256(pool, "UPDATE_ASSET_PARAMS", token)the five requestAssetParams fields; queued only by requestAssetParams, not by requestOp
UPDATE_FEESTUNINGkeccak256(pool, "UPDATE_FEES")IPool.FeeParams (carries flowCooldownSecs; a lowering rides here)
ADD_ASSETLISTINGkeccak256(pool, "ADD_ASSET", token)IAdmin.AddAssetPayload
UPDATE_CURVELISTINGkeccak256(pool, "UPDATE_CURVE", curveId)(uint256[] interior, int256[] wQ, uint16 dispRefPbps, uint8 flags)
UPDATE_HOOKLISTINGkeccak256(pool, "UPDATE_HOOK", token)(address hook, uint32 flags)
UPDATE_TREASURYGOVERNANCEkeccak256(pool, "UPDATE_TREASURY")address newTreasury
SWEEPGOVERNANCEkeccak256(pool, "SWEEP", token)address token; sweeps an unlisted token or stray native to treasury(), the amount is recomputed at executeSweep
UPDATE_ANCHORGOVERNANCEkeccak256(pool, "UPDATE_ANCHOR", token)(address anchor, IPool.OracleConfig cfg); re-anchor + oracle cfg, atomic
MIGRATE_BASE_TOKENGOVERNANCEkeccak256(pool, "BASE_MIGRATION")address newBase
BACKFILL_LEGSGOVERNANCEkeccak256(pool, "BACKFILL_LEGS", keccak256(abi.encode(legs)))address[] legs, the roster executeBackfillLegs writes (arms pool-level solvency, one-shot); refused on a pool already armed
UPDATE_POOL_ADMINGOVERNANCEkeccak256(pool, "UPDATE_POOL_ADMIN")address newSeat, consumed by acceptPoolAdmin(pool) (GEN-1 seat handover)

Every payload is validated at execute, against the state on the day it lands. A malformed payload costs the request window and writes nothing. The tier table is exhaustive and reverts rather than defaulting: an unknown opType cannot be queued.

Actions:

  1. Compute operation key keccak256(pool, "ADD_ASSET", token)
  2. Pack timelock: [executeAt:48][grace:48]
  3. Store pending data
  4. Emit TimelockRequested

4.2. Step 2: Wait

The tier delay must pass and execution must land inside the grace period (§3).

4.3. Step 3: Execute

function executeAddAsset(address pool, address token) external

Actions:

  1. Validate timelock (delay passed, within grace)
  2. Decode and validate pending data
  3. Apply changes
  4. Clear pending state
  5. Emit completion event

4.4. Step 4: Cancel (Optional)

function cancelOp(address pool, uint8 opType, bytes32 subject) external

One entrypoint cancels any queued op: guardian or owner on a pool with no seat (the protocol sentinel), only the seat on a seated pool. subject is the third key component: the asset address for token-keyed ops (left-padded, bytes32(uint256(uint160(token)))) or the curve id for UPDATE_CURVE. It is ignored by the four pool-level ops (MIGRATE_BASE_TOKEN, UPDATE_TREASURY, UPDATE_FEES, UPDATE_POOL_ADMIN), which key on (pool, opId) alone. An unknown opType reverts rather than cancelling an unrelated key.


5. Asset Management

5.1. Add Asset

struct AddAssetPayload { IPool.OracleConfig oracleCfg; IPool.RiskConfig riskCfg; uint16 curveId; uint16 minFeePbps; uint32 minDispersionPbps; uint16 vegaBps; bytes32 lpSym; // LP receipt SYM } admin.requestOp( pool, uint8(IPool.OpType.ADD_ASSET), bytes32(uint256(uint160(token))), abi.encode(IAdmin.AddAssetPayload(oracleCfg, riskCfg, curveId, minFeePbps, minDispersionPbps, vegaBps, lpSym)) ); admin.executeAddAsset(pool, token); // after the LISTING delay (1 day)

The token is not in the payload: it is the subject and the executeAddAsset argument, so the two can never disagree. No initialPrice or vol-EMA seeds: prices and σ live entirely in the mark store, the pool holds no price state to seed. curveId points into the pool’s shared curve table; install the curve first via setCurve (pre-seal) or a queued UPDATE_CURVE.

Exactly two pricing sensitivities are passed at listing: minDispersionPbps, the quiet-tape band floor, and vegaBps, this leg’s σ-sensitivity slope (BPS = 1x). Inventory skew has no per-asset parameter: it is fixed protocol-wide (PricingLib.computeInventorySkew, Inventory Management §3), so its bounds hold on every leg without configuration.

Those two parameters set the dispersion band. minDispersionPbps is the leg’s fitted floor, σ scales the band up from it one-for-one at vegaBps = BPS (1.0x, PricingLib._calculateDispersion), and the ceiling is structural rather than per-asset: the protocol constant MAX_DISPERSION_PBPS = 900_000. Separately, PricingLib.dispersionCap(curve), the widest dispersion whose interior mid swing still fits MAX_INTERIOR_SWING_PBPS = 10_862 PBPS (a full swing of 1.0862% of mark, so ±0.5431% of interior mid displacement), bounds the floor at the write via PoolConfigLib.sanitizeDispersion, on interior-capable legs only. A leg that other legs anchor to therefore never lists with a quiet-tape band its own shape cannot fence, and a σ-driven κ above that cap fails closed on the swing rather than mispricing.

After listing, a queued UPDATE_PROFILE op (executeUpdateProfile) re-sets the band in the same write that re-points curveId, and raiseMinDispersion raises the floor alone, instantly, at the live curveId (§5.3). decimals is not an argument: it is read from IERC20Metadata(token).decimals() at listing and must land in 1..18. lpSym is the last field and is listed, not read from token.symbol(): 1..8 characters of [A-Za-z0-9], NUL-padded, unique within the pool. It becomes the SYM of the receipt (B{code}-{SYM}, Providing Liquidity); wraps keep the W (BC-WBNB).

Defaults set on execution:

  • anchor = baseToken, the shallow default, overridable via a queued UPDATE_ANCHOR
  • liquidityIndexWad = 1e18

addAsset(pool, token, oracleCfg, riskCfg, curveId, minFeePbps, minDispersionPbps, vegaBps, lpSym) exists as the pre-seal bootstrap variant; sealBootstrap(pool) permanently closes it (GOV-03).

Validation (at execute, PoolConfigLib.initAsset + PoolConfigLib):

  • lpSym well-formed (1..8 [A-Za-z0-9], NUL-padded) and not already used by another leg of the pool
  • Token not already configured; decimals in 1..18 (0 collides with the not-configured sentinel, >18 underflows the quote scaling)
  • Curve exists. curveId = 0 is a refused-at-config sentinel, not a routing branch: there is no empty-curve fallback and no linear-impact quote, so every listed leg carries a real curve
  • The curve’s flags byte is an off-chain codebook key. Nothing on chain reads it, and validateCurveAssign checks curve existence only: the wall is unconditional, so there is no flag-conditional gate
  • Oracle config valid, same rules as an oracle update (§7.1)
  • MIN_FEE_PBPS (1) <= minFeePbps <= ONE_PCT_PBPS (10_000 PBPS = 1%)
  • vegaBps >= 1. There is no 1.0x default: PoolConfigLib.validateAssetParams reverts InvalidInput on 0 and the value is written literally. A payload passing 0 queues fine and reverts at execute, costing the LISTING window
  • Every listed asset including the base/hub carries MIN_KAPPA_COV_BPS (50) <= kappaCovBps <= BPS (10,000) (PoolConfigLib.requireNeverDepletable), and a spoke’s κ never exceeds its hub’s (requireHubKappaDominant); _covToll is output-only, so hub κ prices taking the hub out. Canonical statement: Invariants §I-9

riskCfg.depositCapCode must carry a non-zero mantissa (code >> 4 != 0): every listing is capped from birth (§5.3). minFeePbps is the only fee rate a leg carries, so the two-sided MIN_FEE_PBPS/ONE_PCT_PBPS bound above is what stops an admin key pinning a 6.55% floor in one write.

5.2. Set Asset Params (owner-immediate)

function requestAssetParams( address pool, address token, uint128 minLiquidity, uint16 minFeePbps, uint16 vegaBps, uint16 coopDiscBps, // BPS of the LP fee a coop fill is discounted, <= MAX_COOP_DISC_BPS (3000) uint16 coopRebateBps // BPS of the coop caller's gain returned to LPs, <= BPS ) external

These five fields are the only hot asset params routed through Admin. Everything else about a listed leg is either structural (oracle mode, curve, anchor, risk config) and timelocked, or not writable at all.

One per-leg write sits outside Admin entirely: Pool.adminSetDeadSeedPow10(token, pow10) gates on AccessControl.owner() directly, is untimelocked, and does not route through this contract. It sets the dead-share seed as a power of ten of the token’s own unit (0 = the decimals-derived default), is bounded at decimals + DEAD_SEED_POW10_HEADROOM (3, so 1000 whole tokens) and takes effect only while the leg is still unseeded. The gate differs because the Admin singleton is pinned by PoolFactory’s immutable check and cannot grow a forwarder.

Whether a write lands instantly or queues is decided inside requestAssetParams (GOV-ECO-01), not by a separate entrypoint. Pre-seal, or a defensive tighten (minLiquidity unchanged, minFeePbps and vegaBps non-decreasing, coopDiscBps non-increasing, coopRebateBps non-decreasing), applies immediately. Anything else queues at the TUNING tier as OpType.UPDATE_ASSET_PARAMS, landing via executeUpdateAssetParams(pool, token). The queued blob carries a request-time snapshot of all five fields; at execute each is compared-and-swapped, so a field this op moves that moved underneath reverts AssetParamMoved(field, queued, live). UPDATE_ASSET_PARAMS is not requestable through requestOp.

Validation:

  • MIN_FEE_PBPS (1) <= minFeePbps <= ONE_PCT_PBPS
  • vegaBps >= 1
  • a vegaBps or minDispersionPbps raise on an interior-capable leg must keep minDispersionPbps + σ·vegaBps/BPS at the leg’s live σ under PricingLib.dispersionCap, on every lane; a curve repoint (UPDATE_PROFILE) and a re-anchor (§6.1) are held to the same bound
  • minLiquidity <= type(uint96).max (the Asset slot-1 packing bound)
  • an armed RiskFences.minFeeHardMinPbps floors minFeePbps on this lane too, re-applied at execute; the coop fences (maxCoopDiscBps, minCoopRebateBps, maxCoopRebateBps) bind it as well, and all zero admits only zero coop terms

setAssetParamsBounded is the steward’s fenced twin (same arguments, never queues); clamp rules: Risk Steward §2.

5.3. Update Risk Config

admin.requestOp(pool, uint8(IPool.OpType.UPDATE_RISK), bytes32(uint256(uint160(token))), abi.encode(cfg)); admin.executeUpdateRisk(pool, token);

This writes RiskConfig only, four fields stored in Asset slot 2:

struct RiskConfig { uint16 flags; // feature + halt bits; bit 9 = DEPOSIT_GATED_BIT (depositor allowlist) uint16 kappaCovBps; // convex coverage-wall strength, 50..10_000 on every listed asset 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 }

Live bit layout of flags: Pool §8.

The tier follows the direction, not the field (Parametrization §2). Four lanes write risk, three of them one-way and instant. A κ raise is setRiskConfigTighten with the live config and κ swapped: one entry per field and direction.

LaneDelayRule
setRiskConfigTighten(pool, token, cfg)nonesteward or owner. Every field must tighten against live state read in the same call: an ENABLE bit (swap, liability-swap, flash) may only be cleared, each on its own, DEPOSIT_GATED_BIT may only be set, every other flag identical, κ non-decreasing, cap non-increasing, a live maxLiabWeightBps neither cleared nor raised. Voids a live UPDATE_RISK only when that absolute payload would undo a field this write tightens (κ below the raise, a switch it cleared back on, a gate it armed cleared, cap or weight above one it cut); a payload that moves only fields the write restated survives, and the write still lands. requestOp refuses any static payload that is not its exact encoded length, and queued words are read raw, so an out-of-range payload is skipped, never decoded. Every re-enable queues
raiseMinDispersion(pool, token, minDispersionPbps)nonedispersion floor raise only, steward or owner, at the leg’s live curveId. Voids a live UPDATE_PROFILE whose floor, read as it will land (0 = the 1000 default), is lower; a queued floor at or above it survives beside the raise. A cut, and every curve move, stay on UPDATE_PROFILE
narrowRefBand(pool, token, refBandBps)nonedepeg band narrow only (MIN_REF_BAND_BPS (50) <= new < live: below that a narrow is a 7-day freeze, which is the halt’s job), steward or owner. Clamps a live UPDATE_ANCHOR that keeps the live lane and carries a wider band to the narrowed band in place, announced as QueuedRefBandClamped: the op keeps its eta, so a narrow is never a veto over a re-anchor. A queued op that moves the lane or disarms the band (0) is structural and is left alone. Widening, disarming and every other oracle field stay on UPDATE_ANCHOR at GOVERNANCE
requestOp(UPDATE_RISK) → executeUpdateRiskTUNING, 1 hany direction; a cap raise is bounded at 10× the live cap per op. Re-enabling a switch lands here

Two rules bind every write:

  • Halt bits survive it. The executor re-applies the pre-write HALT_MASK bits over the payload, so a halt raised during the timelock window cannot be cleared, or sneaked in, by executing a queued RiskConfig. Only unhaltAsset and batchRiskOp with BatchOp.Unhalt touch those bits.
  • κ never leaves [MIN_KAPPA_COV_BPS, BPS] (requireNeverDepletable), and a spoke’s κ may not exceed its hub’s (requireHubKappaDominant).

5.4. Update Fee Params

admin.requestOp(pool, uint8(IPool.OpType.UPDATE_FEES), bytes32(0), abi.encode(params)); admin.executeUpdateFees(pool);

Updates protocol share, flash loan fee and the anti-JIT cooldown. Pool-wide: subject is ignored. setFlowCooldown raises the cooldown instantly and voids a live UPDATE_FEES carrying a shorter one; the owner re-queues the rest of that payload.

5.5. Set / Clear Asset Hook

function requestOp(address pool, uint8 opType /* UPDATE_HOOK */, bytes32 subject, bytes calldata payload) external function executeUpdateHook(address pool, address token) external function cancelOp(address pool, uint8 opType, bytes32 subject) external function clearAssetHook(address pool, address token) external

Timelocked install/replace of the per-asset IPoolHooks target + flags. Queue via the generic requestOp with UPDATE_HOOK, subject = bytes32(uint256(uint160(token))) and payload abi.encode(address hook, uint32 flags); cancel with cancelOp(pool, opType, subject). Only the executor is typed. clearAssetHook is immediate, steward or owner. It syncs accrued yield first, voids a queued UPDATE_HOOK install, then recalls what the venue can return. All of it back: the hook is dropped. Recall rounding dust (at most invested/1e9 + 1) is written off reserves (HookDustWrittenOff). A remainder (paused redeem, short cash), or a sync that reverts (a halted leg holding a gain, or venue value the sync could not credit, Hooks §5.2): the hook stays attached recall-only (HOOK_RECALL_ONLY: no invest) until a later clear finishes; a hook that never returns it leaves the queued force-clear (UPDATE_HOOK to address(0), LISTING). A latched slot keeps a queued re-install: that op is what lifts the latch. See Hooks.


6. Anchor Tree Management

6.1. Set Anchor

admin.requestOp( pool, uint8(IPool.OpType.UPDATE_ANCHOR), bytes32(uint256(uint160(token))), abi.encode(anchor, cfg) ); admin.executeUpdateAnchor(pool, token);

The anchor and the oracle config travel as one payload at the GOVERNANCE tier and are never separable. There is no untimelocked re-anchor path: a parent can be any asset in the tree, so a re-anchor is a repricing, not a relabel.

Validation (AnchorTreeLib.validateAnchor), shipped:

  • Asset must exist
  • anchor = address(0) only for the base token (depth 0)
  • Every other asset anchors to exactly one parent, which may be any asset in the tree, not only the base
  • The chain must reach the root within MAX_DEPTH = 4: deeper reverts DepthExceeded. A path is two such chains meeting at the LCA, so MAX_PATH_LENGTH = 2 * MAX_DEPTH + 1 = 9 nodes and 8 legs
  • Cycles are rejected explicitly: the walk carries a current == asset check and an unconditional step cap, because a disconnected cycle never reaches the root
  • After the write, the new parent (it now has a child) and the re-anchored leg (its σ now comes from the new feed) must each fit PricingLib.dispersionCap at live σ when interior-capable (PoolConfigLib.requireInteriorFits)

Effects:

  • Updates asset.anchor and the leg’s OracleConfig in the same write
  • Emits AnchorUpdated(pool, asset, anchor). There is no stored anchorDepth field: depth is walked from anchor on demand
  • Swap paths recomputed on next swap

Gating. Re-anchoring re-roots a subtree, so it sits at the base-migration timelock tier (OpType.UPDATE_ANCHOR, GOVERNANCE) and is atomic with the oracle config. Re-anchoring X to P while X’s feed is still attested in the old units misprices that leg by the parent’s price and can drain the reserve in one block. collapseAnchor is the guardian emergency path: untimelocked, one-way toward the root, and it halts the leg in the same write. That direction fails safe, since the root feed always exists and a shorter tree only lengthens paths, which raises the summed fee.

A pool’s anchor column is its actual topology, and filling a cell deepens that pool’s tree through a timelocked UPDATE_ANCHOR op. What a deep tree provides and what a deep edge requires: Pool §7.


7. Oracle Configuration

7.1. OracleConfig

A leg’s OracleConfig is written at listing (AddAssetPayload.oracleCfg) and by UPDATE_ANCHOR (§6.1). There is no standalone repoint op: the config names a store lane, and the store moves only with the Pool implementation, through the fleet upgrade (PoolFactory.executeReferenceUpgrade, GOVERNANCE, which calls migrateFrom before any pool moves). Field order matters. Below is the on-chain declaration order in IPool.sol; a positional abi.encode against a reordered copy encodes wrong values that only revert at execute, costing the window.

struct OracleConfig { uint8 lane; // Store lane (< 64) carrying the leg's primary (P) and reference (R) tiers uint8 mode; // 0 = EXTERNAL: quote = the lane's P mark; 1 = INTERNAL: fixed 1.0 peg, P mark = depeg gate uint8 quoteUnit; // 0 = QUOTE_UNIT_ANCHOR (the norm, no re-denomination), 1 = QUOTE_UNIT_UOA bridge uint16 refBandBps; // Band of P against the lane's R tier; required on spokes (<= 500), 0 on the base }

The config packs into Asset.oracleBits. (There is no feed id, oracle address, secondary feed, modeFlags, or accDecimals field.)

quoteUnit = 1 is the unit-of-account bridge: the mark is attested as <TOKEN>-USD and the pool divides out the base’s own USD price at consumption. It is legal only while the asset anchors directly to the base, since the correction divides by the base price; any deeper leg must attest anchor-per-child (quoteUnit = 0) or the composition is dimensionally wrong. The base itself must be quoteUnit = 0: it is the USD reference, so flagging it would ask the pool to divide the base mark by itself.

INTERNAL mode reads no stored peg field; there is no Asset.pegB64. It quotes a synthetic, never-stale peg at mark 1.0 with STABLE_SIGMA_PBPS (MarkWordLib.peg), while refBandBps stays armed, so the lane’s P mark remains the depeg breaker rather than the price source. Mode selection: Mark Relayer; consumer-side read shape: Price feed reads.

Validation:

  • lane below 64 and registered in the store
  • Every non-base spoke must arm the band (refBandBps != 0, at most MAX_EXTERNAL_REF_BAND_BPS = 500), in both modes. The reference is the lane’s R tier, signed by its own roster; independent signer and admin failure domains between the two tiers remain a deployment invariant
  • The base must carry refBandBps = 0; its depeg breaker is the parity halt, whose band is the base’s depeg code x 250 bps, at most 2,500 (code 0 reads as 2,500)
  • No two nodes of one anchor chain may share a lane, and the chain’s summed refBandBps is at most MAX_ROUTE_REF_BAND_BPS = 600
  • INTERNAL is refused on the base token, and its ref band must satisfy refBandBps <= MAX_STABLE_DEPEG_BAND_BPS (50 bps); EXTERNAL bands are not bound by that constant
  • quoteUnit = 1 requires mode = EXTERNAL and anchor == baseToken

8. Fee Collection

8.1. Claim Treasury

function claimTreasury(address pool, address token) external

msg.sender must equal pool.treasury(). The destination is the treasury itself, never an argument. The call runs Pool.adminClaimTreasury(token): it folds untracked balance into the leg as treasury shares (capped at the leg’s liabilities, skipped on exotic legs), then mints the pending Pool.treasuryShares(token) to the treasury as the leg’s LP token, and emits TreasuryClaimed(pool, token, shares). Operator guide: Protocol Fee Collection.


9. Governance Operations

9.1. Ownership Transfer

Ownership lives on the shared AccessControl singleton: Admin has no ownership-transfer functions. Every pool-admin gate (_onlyPoolAdmin) admits the pool’s seat (pool.poolAdmin()), or AccessControl.owner() when the seat is the zero sentinel; acceptPoolAdmin completes a seat handover.

9.2. Pool Reference-Impl Swap

Pool upgrades live at the PoolFactory, not at Admin, and they are fleet-wide: one timelocked fleet upgrade moves every live pool to the new implementation (Admin).

factory.requestReferenceUpgrade(address newImpl); factory.executeReferenceUpgrade(); factory.cancelReferenceUpgrade();

See Admin for the full flow.

9.3. Base Token Migration

admin.requestOp(pool, uint8(IPool.OpType.MIGRATE_BASE_TOKEN), bytes32(0), abi.encode(newBase)); admin.executeMigrateBaseToken(pool, legs); // legs = every non-base leg, re-denominated atomically

Changes the pool’s root anchor. GOVERNANCE tier, 7 days. Pool-wide: subject is ignored. Queueing this op does not halt trading; halt is a separate haltAsset call.

9.4. Treasury Update

admin.requestOp(pool, uint8(IPool.OpType.UPDATE_TREASURY), bytes32(0), abi.encode(newTreasury)); admin.executeUpdateTreasury(pool);

Changes the pool’s protocol-fee sink. Pool.treasury() is a plain address (a multisig), not a contract. Pool-wide: subject is ignored.


10. Timelock Storage

10.1. Packed Format

// Single uint96 per operation mapping(bytes32 => uint96) pendingOps; // [executeAt:48][grace:48] mapping(bytes32 => bytes) pendingData; // Operation parameters

10.2. Validation

function validate(uint96 packed) internal view { uint48 eta = uint48(packed >> 48); uint48 grace = uint48(packed); if (eta == 0 || block.timestamp < eta) revert Err.NotReady(); if (grace > 0 && block.timestamp > eta + grace) revert Err.Expired(); }

An unqueued op and a not-yet-mature op share one error, NotReady. Re-queueing over a live lock reverts AlreadyPending. A lock already past eta + grace is overwritten instead (the complement of validate’s Expired arm, exposed as TimelockLib.isLive), emitting TimelockCancelled then TimelockRequested with a full fresh delay (Admin).


11. Events

All events carry the target pool (per-pool keyed singleton):

// Timelock lifecycle event TimelockRequested(address indexed pool, bytes32 indexed id, uint8 opType, uint48 executableAt); event TimelockCancelled(address indexed pool, bytes32 indexed id, uint8 opType); // Asset management event AssetAdded(address indexed pool, address indexed token, uint8 decimals, uint128 minLiquidity); event AssetParamsUpdated(address indexed pool, address indexed token, uint128 minLiquidity); event RefBandUpdated(address indexed pool, address indexed token, uint16 refBandBps); event QueuedRefBandClamped(address indexed pool, bytes32 indexed id, uint16 refBandBps); event PoolAdminUpdated(address indexed pool, address indexed oldAdmin, address indexed newAdmin); event RiskConfigUpdated(address indexed pool, address indexed token, uint16 flags); event FeeParamsUpdated(address indexed pool, uint8 protoSharePct, uint16 flashFeePbps); event ProfileUpdated(address indexed pool, address indexed token); event CurveUpdated(address indexed pool, uint16 indexed curveId); event AssetHookUpdated(address indexed pool, address indexed token, address hook, uint32 flags); event BootstrapSealed(address indexed pool); event FlowCooldownUpdated(address indexed pool, uint16 newCooldownSecs); // Risk steward event RiskFencesUpdated(address indexed pool, address indexed token, uint16 maxDeltaBps); event BoundedAssetParamsUpdated(address indexed pool, address indexed token, uint16 minFeePbps, uint16 vegaBps, bool tighten); // Governance event BaseTokenMigrated(address indexed pool, address indexed oldBase, address indexed newBase); event TreasuryUpdated(address indexed pool, address indexed oldTreasury, address indexed newTreasury); // Emergency (src = the HALT_MASK bit set/cleared, `HALT_MASK` = 0x0005) event AssetHalted(address indexed pool, address indexed token, uint16 indexed src); event AssetUnhalted(address indexed pool, address indexed token, uint16 indexed src); event RiskOpsBatched(address indexed pool, address indexed token, uint8 op, uint16 src); event BatchLegSkipped(address indexed pool, address indexed token); // Fees event TreasuryClaimed(address indexed pool, address indexed token, uint256 shares); // Anchor tree event AnchorUpdated(address indexed pool, address indexed asset, address indexed anchor); // Upgrade gate (Admin's own UUPS implementation) event UpgradeRequested(bytes32 indexed id, address impl, uint48 executableAt); event UpgradeCancelled(bytes32 indexed id); event Upgraded(address indexed impl); event Paused(bool paused);

(There is no ModulesUpdated event: Admin has no module registry. AnchorUpdated carries no depth field, since depth is not stored. Ownership events live on AccessControl.)


12. Security Considerations

12.1. Grace Period Protection

The grace period stops a matured operation sitting in the queue indefinitely: a request never executed expires instead of staying executable indefinitely.

12.2. Multi-sig Recommendation

The owner should be a multi-signature wallet with geographically distributed signers on hardware wallets. Threshold must satisfy QuorumLib.admin(n) = ceil(2n/3): 2-of-3, 3-of-4, 4-of-5, 5-of-7. A 3-of-5 or 4-of-7 Safe is rejected by QuorumLib.checkAdmin. See Access Control.

12.3. Emergency Response

  1. Immediate: haltAsset() on affected tokens (guardian or owner), or batchRiskOp for a sweep
  2. Assess: damage and root cause
  3. Plan: prepare the fix with its timelock
  4. Execute: apply after the delay
  5. Monitor: unhalt and watch

Admin.pause() (guardian or upgrade authority, instant) freezes the execution of a matured Admin upgrade; unpause() is authority-only. It does not touch pool operations, and there is no pool-wide pause bit.


13. Code References

See Admin.sol (singleton, payload encode/apply) + Pool.sol (the admin* restricted entry points) + PoolConfigLib.sol (validation + the writes themselves).

Every restricted setter above is a function on Pool itself: adminHaltAsset/adminUnhaltAsset/adminInitAsset/adminSetAssetParams/adminSetRiskConfig/adminSetProfile/adminSetCurve/adminSetAnchor/adminCollapseAnchor/adminSetFlowCooldown/adminSetFeeParams/adminSetTreasury/adminSetBaseToken/adminSetAssetHook/adminClearAssetHook/adminSweep/adminBackfillLegs, adminSetPoolAdmin/adminNarrowRefBand/adminForceClearHook/adminClaimTreasury, plus flashSend/flashAccount (Pool.sol). Each is gated onlyAdminContract and DELEGATECALLs the linked PoolConfigLib library, which takes $ as a storage parameter, so the compiler resolves the slots. The one exception is adminSetDeadSeedPow10, which carries the same admin prefix but gates on AccessControl.owner() directly (§5.2). Fixed at compile time, not a runtime-pluggable module (see Overview §2.1).


PageCovers
Access ControlFull governance architecture, tier durations
AdminFactory reference-impl swap, UUPS singletons, timelock mechanics
GuardianHalt and cancel runbook
Risk StewardsetAssetParamsBounded clamp rules
Pool Deployment & CurationListing a pool and its legs end to end
PoolThe admin* entry points these calls land on