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 onlyEffects:
- Sets or clears the given bit in the leg’s risk-config flags
- Any bit in
HALT_MASKblocks 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) externalBatchOp 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.
function requestOp(address pool, uint8 opType, bytes32 subject, bytes calldata payload) externalopTypeisIPool.OpType, cast touint8. The ordinal is the wire value, so read it off the enum and never off a prose list.IPool.soldeclares it grouped by timelock tier:Ordinal OpTypeOrdinal OpType0 NONE6 ADD_ASSET1 MIGRATE_BASE_TOKEN7 UPDATE_RISK2 UPDATE_ANCHOR8 UPDATE_FEES3 UPDATE_TREASURY9 UPDATE_PROFILE4 UPDATE_HOOK10 UPDATE_CURVE5 UPDATE_ORACLE11 UPDATE_ASSET_PARAMSNONEandUPDATE_ASSET_PARAMSare not requestable and revert;UPDATE_ASSET_PARAMSis queued only bysetAssetParamsitself. Encoding a literal off a stale ordering is a live mis-routing hazard: a caller that sends2intendingUPDATE_ORACLEqueuesUPDATE_ANCHOR, at theCRITICALtier.subjectis the third key component:bytes32(uint256(uint160(token)))for token-keyed ops,bytes32(uint256(presetId))forUPDATE_CURVE, ignored by the three pool-wide ops (MIGRATE_BASE_TOKEN,UPDATE_TREASURY,UPDATE_FEES).payloadisabi.encodeof the op’s arguments without the subject. Structs live inIAdmin.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:
- Compute operation key
keccak256(pool, "ADD_ASSET", token) - Pack timelock:
[executeAt:48][grace:48] - Store pending data
- Emit
TimelockRequestedevent
4.2. Step 2: Wait
- Minimum delay must pass
- Within grace period
4.3. Step 3: Execute
function executeAddAsset(address pool, address token) external onlyAdminActions:
- Validate timelock (delay passed, within grace)
- Decode and validate pending data
- Apply changes
- Clear pending state
- Emit completion event
4.4. Step 4: Cancel (Optional)
function cancelTimelock(address pool, uint8 opType, bytes32 subject) externalOne 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 delayThe 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 queuedUPDATE_ANCHORhaircutSuppressorBps = BPS, forced to 0 ifkappaCovBps > 0liquidityIndexWad = 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;
decimalsin 1..18 (0 collides with the not-configured sentinel, >18 underflows the quote scaling) - Preset curve exists.
presetId = 0is 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.validateAssetParamsrevertsInvalidInputon 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;_covTollis 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 onlyAdminThis 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_PBPShaircutSuppressorBps < HAIRCUT_SUPPRESSOR_FULL_BPS(20_000), strictly: the exit haircut can never be fully switched offkappaCovBps > 0requireshaircutSuppressorBps == 0minLiquidity <= type(uint96).max(theAssetslot-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_MASKbits over the payload, so a halt raised during the timelock window cannot be cleared (nor sneaked in) by executing a queuedRiskConfig. OnlyunhaltAsset(andbatchRiskOpwithBatchOp.Unhalt) touches those bits. - κ cannot be stripped from a wall-gated preset. Setting
kappaCovBps = 0on an asset whose preset carriesFLAG_REQUIRES_WALLreverts.
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 onlyAdminTimelocked 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 revertsDepthExceeded. A path is two such chains meeting at the LCA, soMAX_PATH_LENGTH = 2 * MAX_DEPTH + 1 = 9nodes and 8 legs - Cycles are rejected explicitly: the walk carries a
current == assetcheck and an unconditional step cap, because a disconnected cycle never reaches the root
Effects:
- Updates
asset.anchorand the leg’sOracleConfigin the same write - Emits
AnchorUpdated(pool, asset, anchor). There is no storedanchorDepthfield: depth is walked fromanchoron 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:
primaryset and callable- Armed ref band (
refBandBps != 0) requiresrefFeedIdplus a reachablerefPrimary != 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 = 1requiresmode = EXTERNALandanchor == baseToken
8. Fee Collection
8.1. Collect Protocol Fees
function collectProtocolFees(address pool, address token) externalAuthorization: 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:
- Read accumulated fees
- Clear fee counter
- 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 atomicallyChanges 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 parameters10.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 byQuorum.checkAdmin. See Access Control & Roles - Geographically distributed signers
- Hardware wallet usage
12.3. Emergency Response
For security incidents:
- Immediate:
haltAsset()on affected tokens (guardian or owner), orbatchRiskOpfor a sweep - Assess: Evaluate damage and root cause
- Plan: Prepare fix with timelock
- Execute: Apply fix after delay
- 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).
14. Related Documentation
- Access Control, Roles & Emergency Powers: Full governance architecture
- Deployment & Upgrades: Upgrade process (factory reference-impl + UUPS singletons)