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 governance contract serving every pool. Each public function takes address pool as its first arg. Owner authority routes through the shared singleton AccessControl. Admin 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 (e.g. pool.adminHaltAsset(token, src)) are standard external calls that Pool answers directly; Pool then DELEGATECALLs the linked PoolConfig library for the state mutation, a compile-time-fixed target, not a runtime-selectable module (see §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

One pair of calls covers both halt sources. src is the HALT_MASK bit (or bits) being set or cleared: HALT_RISK_BIT (bit 0, the owner risk halt) or HALT_GUARDIAN_BIT (bit 6, the guardian emergency halt).

function haltAsset(address pool, address token, uint16 src) external // guardian OR owner function unhaltAsset(address pool, address token, uint16 src) external // owner only

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)

Sources refcount. unhaltAsset(src) clears only that bit, so lifting a fleet-wide guardian halt can never relist a leg that an owner risk halt still holds down. A leg is tradeable again only once every source that halted it has been lifted. Authority is by edge, not by source: halting is guardian-or-owner for every src, un-halting is owner-only for every src.

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

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 (works from EOA or multisig, no Safe MultiSend needed), carrying the same authority asymmetry: guardians get the halt edge only. Per-leg try/catch: a bad leg is skipped and logged (BatchLegSkipped) so one failure never bricks an emergency sweep. There is still no atomic pool-wide pause bit: protocol-wide halts enumerate assets off-chain and batch.


3. Timelock Delays

OperationDelayRisk LevelKey
Add Asset1 hourLOWkeccak256(pool, "ADD_ASSET", token)
Update Risk Config1 hourLOWkeccak256(pool, "UPDATE_RISK", token)
Update Profile (preset repoint)1 hourLOWkeccak256(pool, "UPDATE_PROFILE", token)
Set Curve (preset install/refit)1 hourLOWkeccak256(pool, "UPDATE_CURVE", presetId)
Update Fee Params1 hourLOWkeccak256(pool, "UPDATE_FEES")
Set Asset Hook3 daysHIGHkeccak256(pool, "UPDATE_HOOK", token)
Update Oracle2 daysBASEkeccak256(pool, "UPDATE_ORACLE", token)
Update Treasury3 daysHIGHkeccak256(pool, "UPDATE_TREASURY")
Update Anchor (re-anchor + oracle cfg, atomic)7 daysCRITICALkeccak256(pool, "UPDATE_ANCHOR", token)
Migrate Base Token7 daysCRITICALkeccak256(pool, "BASE_MIGRATION")

(No module or ownership timelocks live in Admin: there is no module registry, and ownership sits on the shared AccessControl singleton.)

Delays are deploy-time data, not code. Constants.Tier is CRITICAL, HIGH, BASE, LOW, UPGRADE, ROTATION, FACTORY; a packed schedule word is passed to the AccessControl constructor and read back through Constants.delayOf. Constants.PROD_DELAYS is the production word: CRITICAL 7d, HIGH 3d, BASE 2d, LOW 1h, UPGRADE 7d, ROTATION 7d, FACTORY 14d. LOW sits exactly on MIN_ARMED_DELAY (1 h), the shortest delay an armed deployment may carry. No contract branches on block.chainid.

Grace Period: 7 days after timelock expires to execute. Operations expire if not executed within grace period.


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, so read it off the enum and never off a prose list. IPool.sol declares it grouped by timelock tier:

    OrdinalOpTypeOrdinalOpType
    0NONE6ADD_ASSET
    1MIGRATE_BASE_TOKEN7UPDATE_RISK
    2UPDATE_ANCHOR8UPDATE_FEES
    3UPDATE_TREASURY9UPDATE_PROFILE
    4UPDATE_HOOK10UPDATE_CURVE
    5UPDATE_ORACLE11UPDATE_ASSET_PARAMS

    NONE and UPDATE_ASSET_PARAMS are not requestable and revert; UPDATE_ASSET_PARAMS is queued only by setAssetParams itself. Encoding a literal off a stale ordering is a live mis-routing hazard: a caller that sends 2 intending UPDATE_ORACLE queues UPDATE_ANCHOR, at the CRITICAL tier.

  • subject is the third key component: bytes32(uint256(uint160(token))) for token-keyed ops, bytes32(uint256(presetId)) for UPDATE_CURVE, ignored by the three pool-wide ops (MIGRATE_BASE_TOKEN, UPDATE_TREASURY, UPDATE_FEES).

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

opTypetierpayload
ADD_ASSETLOWIAdmin.AddAssetPayload
UPDATE_RISKLOWIPool.RiskConfig
UPDATE_PROFILELOW(uint16 presetId, uint32 minDispersionPbps)
UPDATE_CURVELOW(uint256[] interior, int256[] wQ, uint16 dispRefPbps, uint8 flags)
UPDATE_FEESLOWIPool.FeeParams
UPDATE_ORACLEBASEIPool.OracleConfig
UPDATE_TREASURYHIGHaddress newTreasury
UPDATE_HOOKHIGH(address hook, uint32 flags)
UPDATE_ANCHORCRITICAL(address anchor, IPool.OracleConfig cfg)
MIGRATE_BASE_TOKENCRITICALaddress newBase

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 event

4.2. Step 2: Wait

  • Minimum delay must pass
  • Within grace period

4.3. Step 3: Execute

function executeAddAsset(address pool, address token) external onlyAdmin

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 cancelTimelock(address pool, uint8 opType, bytes32 subject) external

One entrypoint cancels any queued op, guardian or owner. subject is the third key component: the asset address for token-keyed ops (left-padded, bytes32(uint256(uint160(token)))) or the preset id for UPDATE_CURVE. It is ignored by the three pool-level ops (MIGRATE_BASE_TOKEN, UPDATE_TREASURY, UPDATE_FEES), 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 presetId; uint16 minFeePbps; uint32 minDispersionPbps; uint16 vegaBps; } admin.requestOp( pool, uint8(IPool.OpType.ADD_ASSET), bytes32(uint256(uint160(token))), abi.encode(IAdmin.AddAssetPayload(oracleCfg, riskCfg, presetId, minFeePbps, minDispersionPbps, vegaBps)) ); admin.executeAddAsset(pool, token); // after the LOW delay

The token is not in the payload: it is the subject and the executeAddAsset argument, so the two can never disagree. No initialPrice / vol-EMA seeds: prices and σ live entirely on ExternalOracle feeds, the pool holds no price state to seed. presetId points into the pool’s shared preset-curve table; the curve must be installed 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 takes no per-asset dial at all: it is a fixed protocol law (Pricing.computeInventorySkew, Inventory Management §3), so its bounds hold on every leg by construction rather than by configuration. The dispersion band those two drive is live: minDispersionPbps is the leg’s fitted floor, σ scales the band up from it one-for-one at vegaBps = BPS (1.0x, Pricing._calculateDispersion), and the ceiling is structural rather than per-asset: the protocol constant MAX_DISPERSION_PBPS = 900_000. Separately, Pricing.dispersionCap(curve) - the widest dispersion whose interior mid swing still fits INTERIOR_SWING_CAP_PBPS = 10_000 PBPS (a full swing of 1% of mark, so ±0.5% of interior mid displacement) - bounds the floor at the write via PoolConfig.sanitizeDispersion, so a leg never lists with a quiet-tape band its own shape cannot fence; a σ-driven κ above that cap still fails closed on the swing rather than mispricing. After listing, only a queued UPDATE_PROFILE op (executeUpdateProfile) re-sets the band explicitly, in the same write that re-points presetId. decimals is not an argument: it is read from IERC20Metadata(token).decimals() at listing and must land in 1..18.

Defaults set on execution:

  • anchor = baseToken, the shallow default, overridable via a queued UPDATE_ANCHOR
  • haircutSuppressorBps = BPS, forced to 0 if kappaCovBps > 0
  • liquidityIndexWad = 1e18

addAsset() (same params) exists as the pre-seal bootstrap variant; sealBootstrap(pool) permanently closes it (GOV-03).

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

  • Token not already configured; decimals in 1..18 (0 collides with the not-configured sentinel, >18 underflows the quote scaling)
  • Preset curve exists. presetId = 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
  • A preset carrying FLAG_REQUIRES_WALL (bit 0) is only assignable to a coverage-walled leg: kappaCovBps > 0 (PoolConfig.validatePresetAssign)
  • The curve’s minimum offset, scaled to the max dispersion, must keep the price multiplier strictly positive
  • 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: PoolConfig.validateAssetParams reverts InvalidInput on 0 and the value is written literally. A payload passing 0 queues fine and reverts at execute, costing the LOW window
  • Every listed asset including the base/hub must have kappaCovBps > 0; _covToll is output-only, so hub κ prices taking the hub out. Canonical statement: Invariants §I-9.

One κ coupling binds a listing: kappaCovBps > 0 forces haircutSuppressorBps == 0 (Lemma B of the published coverage proofs), enforced at both listing and setAssetParams. minFeePbps is the only fee rate a leg carries, so the two-sided MIN_FEE_PBPS/ONE_PCT_PBPS bound above carries the whole job of keeping an admin key from pinning a 6.55% floor in one write.

5.2. Set Asset Params (owner-immediate)

function setAssetParams( address pool, address token, uint128 minLiquidity, uint16 minFeePbps, uint16 vegaBps, uint16 haircutSuppressorBps ) external onlyAdmin

This is the whole hot asset-param surface routed through Admin: four fields. Everything else about a listed leg is either structural (oracle mode, preset, 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, i.e. 1000 whole tokens) and takes effect only while the leg is still unseeded. The gate divergence is deliberate: 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 setAssetParams (GOV-ECO-01), not by a separate entrypoint. Pre-seal, or a defensive tighten (minLiquidity unchanged, minFeePbps non-decreasing, vegaBps non-decreasing, haircutSuppressorBps unchanged), applies immediately. Any weakening queues at the LOW tier as OpType.UPDATE_ASSET_PARAMS and lands via executeSetAssetParams(pool, token). UPDATE_ASSET_PARAMS is not requestable through requestOp.

Validation:

  • MIN_FEE_PBPS (1) <= minFeePbps <= ONE_PCT_PBPS
  • haircutSuppressorBps < HAIRCUT_SUPPRESSOR_FULL_BPS (20_000), strictly: the exit haircut can never be fully switched off
  • kappaCovBps > 0 requires haircutSuppressorBps == 0
  • minLiquidity <= type(uint96).max (the Asset slot-1 packing bound)

setAssetParamsBounded is the risk-steward twin: same four fields, additionally clamped by the per-asset RiskFences and a relative risk-up delta. A defensive tighten (minLiquidity unchanged, minFeePbps non-decreasing, vegaBps non-decreasing, haircutSuppressorBps unchanged) is exempt from the relative clamp. minLiquidity cannot move at all on that path.

5.3. Update Risk Config

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

RiskConfig is the whole of what this writes, and it is two fields:

struct RiskConfig { uint16 flags; // feature + halt bits uint16 kappaCovBps; // convex coverage-wall strength; 0 = off, forbidden on every listed asset including the hub }

Live bit layout of flags: Pool §8.

What this call moves is therefore the feature/halt flag word and κ, nothing else.

Two rules bind the 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 (nor sneaked in) by executing a queued RiskConfig. Only unhaltAsset (and batchRiskOp with BatchOp.Unhalt) touches those bits.
  • κ cannot be stripped from a wall-gated preset. Setting kappaCovBps = 0 on an asset whose preset carries FLAG_REQUIRES_WALL reverts.

5.4. Update Fee Params

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

Updates protocol share and flash loan fee. Pool-wide: subject is ignored.

5.5. Set / Clear Asset Hook

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

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 cancelTimelock(pool, opType, subject). Only the executor is typed. clearAssetHook is immediate and requires invested == 0. 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.executeAnchorUpdate(pool, token);

The anchor and the oracle config travel as one payload and are never separable, at the CRITICAL tier. 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

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 is a re-rooting of a subtree, so it sits at the base-migration timelock tier (OpType.UPDATE_ANCHOR, CRITICAL tier) 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, but 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.

Topology configuration. A pool’s anchor column is its actual topology, and filling a cell deepens that pool’s tree through a timelocked UPDATE_ANCHOR op; see Pool §7 for what the shape buys and what a deep edge requires.


7. Oracle Configuration

7.1. Update Oracle

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

Changes price feed sources. Field order matters: this is the on-chain declaration order in IPool.sol, and a positional abi.encode against a reordered copy produces silent garbage that only reverts at execute, burning the BASE (or, via UPDATE_ANCHOR, CRITICAL) window.

struct OracleConfig { bytes32 feedId; // Mark feed id on `primary` (keccak256(base, quote)) address primary; // IOracle: mark source (EXTERNAL) or depeg gate (INTERNAL) uint8 mode; // 0 = EXTERNAL (recommended: any IOracle), 1 = INTERNAL (cash-collateral peg) uint8 quoteUnit; // 0 = QUOTE_UNIT_ANCHOR (the norm, no re-denomination), 1 = QUOTE_UNIT_UOA bridge uint16 refBandBps; // Symmetric tolerance for the reference band (0 = disabled) bytes32 refFeedId; // Reference feed for the feed-relative depeg band (0 = disabled) address refPrimary; // Oracle serving refFeedId; MUST differ from `primary` when the band is armed }

refBandBps shares the primary slot deliberately, so PoolIOLib.priceBandGuard reads a disarmed band out of a word quoting has already warmed. (There is no secondary feed, modeFlags, or accDecimals field.)

quoteUnit = 1 is the unit-of-account bridge. It means 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 feed at mark 1.0 with STABLE_SIGMA_PBPS (FeedMathLib.getPegFeed), while primary / feedId / refFeedId / refBandBps stay populated and armed, so the configured IOracle remains the depeg breaker rather than the price source. Mode selection: Oracles §1.

Validation:

  • primary set and callable
  • Armed ref band (refBandBps != 0) requires refFeedId plus a reachable refPrimary != primary. Address inequality is checked on-chain; independent signer and admin failure domains remain a deployment invariant
  • Every non-base spoke must arm the band, in both modes
  • 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. Collect Protocol Fees

function collectProtocolFees(address pool, address token) external

Authorization: msg.sender must equal pool.treasury(). The destination is the caller, never an argument, so fees can only move to the address that already owns them.

Flow:

  1. Read accumulated fees
  2. Clear fee counter
  3. Transfer tokens to msg.sender

9. Governance Operations

9.1. Ownership Transfer

Ownership lives on the shared AccessControl singleton: Admin has no ownership-transfer functions, and every onlyAdmin check resolves AccessControl.owner().

9.2. Pool Reference-Impl Swap

Pool upgrades live at the PoolFactory, not at Admin. They are fleet-wide: one timelocked beacon swap re-points every live pool (Deployment & Upgrades §4.1).

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

See 3.2 Deployment & Upgrades §4.1 for the full flow.

9.3. Base Token Migration

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

Changes the pool’s root anchor. CRITICAL tier, longest timelock. 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.executeTreasuryUpdate(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. Shipping in the next release, 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 (Deployment & Upgrades §6.1).


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 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 presetId); event AssetHookUpdated(address indexed pool, address indexed token, address hook, uint32 flags); event BootstrapSealed(address indexed pool); event FlowCooldownUpdated(address indexed pool, uint16 newCooldown); // 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); event OracleUpdated(address indexed pool, address indexed token); // Emergency (src = the HALT_MASK bit set/cleared) event AssetHalted(address indexed pool, address indexed token, uint16 indexed src); event AssetUnhalted(address indexed pool, address indexed token, uint16 indexed src); event BatchRiskOp(address indexed pool, address indexed token, uint8 op, uint16 src); event BatchLegSkipped(address indexed pool, address indexed token); // Fees event ProtocolFeesCollected(address indexed pool, address indexed token, address indexed recipient, uint256 amount); // Anchor tree event AnchorUpdated(address indexed pool, address indexed asset, address indexed anchor);

(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, so a request that was never executed expires rather than becoming a latent write years later.

12.2. Multi-sig Recommendation

Owner should be a multi-signature wallet:

  • Threshold must satisfy Quorum.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 Quorum.checkAdmin. See Access Control & Roles
  • Geographically distributed signers
  • Hardware wallet usage

12.3. Emergency Response

For security incidents:

  1. Immediate: haltAsset() on affected tokens (guardian or owner), or batchRiskOp for a sweep
  2. Assess: Evaluate damage and root cause
  3. Plan: Prepare fix with timelock
  4. Execute: Apply fix after delay
  5. Monitor: Unhalt and watch closely

13. Code References

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

Every restricted setter above is a function on Pool itself: adminHaltAsset/adminUnhaltAsset/adminInitAsset/adminSetAssetParams/adminSetRiskConfig/adminSetOracleConfig/adminSetProfile/adminSetCurve/adminSetAnchor/adminCollapseAnchor/adminSetFlowCooldown/adminSetFeeParams/adminSetTreasury/adminSetBaseToken/adminSetAssetHook/adminClearAssetHook, plus flashPrepare/flashSend/flashAccount (Pool.sol). Each is gated onlyAdminContract and DELEGATECALLs the linked PoolConfig 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).