---
title: "Admin Contract"
description: "Pool configuration, timelock governance, and emergency controls"
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-04"
publish: true
---
# 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](#13-code-references)).

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

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

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

| Operation | Delay | Risk Level | Key |
|-----------|-------|------------|----|
| Add Asset | 1 hour | LOW | `keccak256(pool, "ADD_ASSET", token)` |
| Update Risk Config | 1 hour | LOW | `keccak256(pool, "UPDATE_RISK", token)` |
| Update Profile (preset repoint) | 1 hour | LOW | `keccak256(pool, "UPDATE_PROFILE", token)` |
| Set Curve (preset install/refit) | 1 hour | LOW | `keccak256(pool, "UPDATE_CURVE", presetId)` |
| Update Fee Params | 1 hour | LOW | `keccak256(pool, "UPDATE_FEES")` |
| Set Asset Hook | 3 days | HIGH | `keccak256(pool, "UPDATE_HOOK", token)` |
| Update Oracle | 2 days | BASE | `keccak256(pool, "UPDATE_ORACLE", token)` |
| Update Treasury | 3 days | HIGH | `keccak256(pool, "UPDATE_TREASURY")` |
| Update Anchor (re-anchor + oracle cfg, atomic) | 7 days | CRITICAL | `keccak256(pool, "UPDATE_ANCHOR", token)` |
| Migrate Base Token | 7 days | CRITICAL | `keccak256(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.

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

  | Ordinal | `OpType` | Ordinal | `OpType` |
  |---:|---|---:|---|
  | 0 | `NONE` | 6 | `ADD_ASSET` |
  | 1 | `MIGRATE_BASE_TOKEN` | 7 | `UPDATE_RISK` |
  | 2 | `UPDATE_ANCHOR` | 8 | `UPDATE_FEES` |
  | 3 | `UPDATE_TREASURY` | 9 | `UPDATE_PROFILE` |
  | 4 | `UPDATE_HOOK` | 10 | `UPDATE_CURVE` |
  | 5 | `UPDATE_ORACLE` | 11 | `UPDATE_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`.

| `opType` | tier | payload |
|---|---|---|
| `ADD_ASSET` | LOW | `IAdmin.AddAssetPayload` |
| `UPDATE_RISK` | LOW | `IPool.RiskConfig` |
| `UPDATE_PROFILE` | LOW | `(uint16 presetId, uint32 minDispersionPbps)` |
| `UPDATE_CURVE` | LOW | `(uint256[] interior, int256[] wQ, uint16 dispRefPbps, uint8 flags)` |
| `UPDATE_FEES` | LOW | `IPool.FeeParams` |
| `UPDATE_ORACLE` | BASE | `IPool.OracleConfig` |
| `UPDATE_TREASURY` | HIGH | `address newTreasury` |
| `UPDATE_HOOK` | HIGH | `(address hook, uint32 flags)` |
| `UPDATE_ANCHOR` | CRITICAL | `(address anchor, IPool.OracleConfig cfg)` |
| `MIGRATE_BASE_TOKEN` | CRITICAL | `address 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

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

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

```solidity
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](/docs/1-1-1-inventory-management#3-inventory-skew-coverage--skew)), 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](/docs/1-1-8-invariants#i-9-coverage-toll-is-charge-only-and-terminal-only).

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)

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

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

```solidity
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](/docs/1-2-1-pool).

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

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

```solidity
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](/docs/5-1-3-hooks).

---

## 6. Anchor Tree Management

### 6.1. Set Anchor

```solidity
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](/docs/1-2-1-pool#7-swap-paths-anchor-tree) for what the shape buys and what a deep edge requires.

---

## 7. Oracle Configuration

### 7.1. Update Oracle

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

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

**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

```solidity
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](/docs/3-2-deployment-upgrades#41-pool-beacon-upgrade-poolfactory-7-day-timelock)).

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

See [3.2 Deployment & Upgrades §4.1](/docs/3-2-deployment-upgrades) for the full flow.

### 9.3. Base Token Migration

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

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

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

### 10.2. Validation

```solidity
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](/docs/3-2-deployment-upgrades#61-timelock-mechanics)).

---

## 11. Events

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

```solidity
// 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](/docs/3-1-access-control-roles-emergency-powers)
- 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](/docs/1-overview#21-standalone-singletons--pool-proxies)).

---

## 14. Related Documentation

- [Access Control, Roles & Emergency Powers](/docs/3-1-access-control-roles-emergency-powers): Full governance architecture
- [Deployment & Upgrades](/docs/3-2-deployment-upgrades): Upgrade process (factory reference-impl + UUPS singletons)
