Admin

The owner multisig resolved by every onlyAdmin check across the DEX. Principals and the op → tier map: Access Control.

1. Ownership

transferOwnership and renounceOwnership revert FeatureDisabled(TRANSFER) (AccessControl.sol). Before armQuorumPolicy (Access Control §1.1), ownership moves by Solady’s two-step handover, with a 48 h request expiry; completeOwnershipHandover rejects address(0). After arming, the handover reverts, and the only path is queueRole(OWNER, safe) → GOVERNANCE delay → executeRole(OWNER). The execute re-checks the incoming Safe against the admin quorum policy, and any guardian can cancel while the rotation waits.

2. The owner lane for asset params

Owner retains the unbounded requestAssetParams, exempt from the steward’s 24 h window but not from an armed fee fence: _requireFences applies minFeeHardMinPbps on the owner lane too, and re-applies it at executeUpdateAssetParams in case the fence was armed while the op sat in the queue. Lowering below an armed fence takes two transactions (setRiskFences first). The owner stays out of the fenced lane: it could widen the fences and write inside them in one transaction, bypassing the TUNING queue.

Two fences bind the owner lane: the fee floor and the coop fences (maxCoopDiscBps, minCoopRebateBps, maxCoopRebateBps; all zero admits only zero coop terms). requestAssetParams calls _requireFences and nothing else; the vega hard bounds and maxDeltaBps are read by setAssetParamsBounded alone. An armed RiskFences row is not a bound on the owner. The bounded lane itself: Risk Steward.

3. Upgrade mechanisms

3.1. Fleet upgrade (PoolFactory, 7-day timelock)

Canonical description of the pool upgrade model.

factory.requestReferenceUpgrade(address(newImpl)); // candidate carries the same AC / Admin / Flash immutables // wait the GOVERNANCE tier delay (7 days in production), then execute inside GRACE_PERIOD_SECS (7 days) factory.executeReferenceUpgrade(); // newImpl.migrateFrom(oldImpl), then upgradeTo on every PoolProxy // OR cancel before exec: owner or any AC guardian. factory.cancelReferenceUpgrade();
  • Fleet-wide, not opt-in. Every deployed pool is a PoolProxy, an ERC-1967 proxy holding its implementation in its own slot, whose upgradeTo answers the factory only. executeReferenceUpgrade() first calls migrateFrom(oldImpl) on the new implementation, which takes the chain’s mark store (it lives in the implementation) before any pool reads it, then calls upgradeTo on every proxy; one failed call reverts the whole upgrade, so the fleet never splits. A single owner transaction replaces the executable code of every live pool at once, third-party administered pools included. No per-pool opt-out, no version pinning. This is the protocol’s largest single trust assumption.
  • New-impl compatibility is asserted on request and again at execute: requestReferenceUpgrade requires the candidate to be a contract carrying the same AC / admin / flash immutables as the live impl, a storageVersion at or above the live one, and a mark-store logic whose FACTORY is this factory (PoolFactory.sol). The store logic’s own AC is not pinned; the owner checks it before requesting. Storage-layout compatibility is not on-chain-checkable and is pinned at build time by ArtifactGuards.t.sol, which asserts Pool declares exactly one storage entry ($ at slot 0).
  • Delay: the GOVERNANCE tier of AccessControl.GOV_DELAYS(), read once into the factory’s DELAY_UPGRADE immutable at construction (PoolFactory.sol). Under ConstantsLib.PROD_DELAYS that is 7 days.
  • Grace window: the matured request expires SC.GRACE_PERIOD_SECS (7 days) after its eta and then reverts Err.Expired (PoolFactory.sol). A stale request must be re-requested, with no cancelReferenceUpgrade first: an expired pendingReferencePool is overwritten in place, announced as ReferencePoolUpgradeCancelled then ReferencePoolUpgradeRequested, at the full GOVERNANCE delay (§4.1). Longest tier, so likeliest to age out unnoticed.
  • Cancellable by the owner or any guardian via cancelReferenceUpgrade() (PoolFactory.sol). Residual, stated in the source: a fully compromised owner can revoke the guardian and re-request, so the guardian veto raises the bar rather than being absolute.
  • Monitor ReferencePoolUpgradeRequested and ReferencePoolUpgraded on PoolFactory, and Migrated on the new implementation.
  • PoolStorage field order/types are append-only across versions: live pools keep their storage across a fleet upgrade.

3.2. UUPS upgrades (Admin / Flash)

Admin and Flash are ERC-1967 proxies over implementations inheriting UpgradeGate. A direct upgradeToAndCall reverts Ownable.Unauthorized() even from the authority: _authorizeUpgrade accepts only the matured self-call with the one-shot transient flag set (UpgradeGate.sol). The request/execute timelock is the sole upgrade path.

admin.requestUpgrade(newImpl); // ... wait the GOVERNANCE tier delay (7 days in production), then execute within SC.GRACE_PERIOD_SECS = 7 days admin.executeUpgrade(); // takes no initData // Veto before execution: authority or any AC guardian. admin.cancelUpgrade(); // Freeze a matured request without cancelling it: authority or any AC guardian. admin.pause();
  • Authority is resolved per contract by UpgradeGate._upgradeAuthority(): AccessControl.owner() for Admin and Flash.
  • The candidate implementation must resolve to the same governance root. UpgradeGate._pinGovernanceRoot compares its AC against the live one and reverts at requestUpgrade and again at executeUpgrade, so an AC-mismatched implementation never reaches the proxy. Asymmetric with §3.1: the gate pins AC only, where PoolFactory._validateImplementation pins AC, admin and flash.
  • The upgrade-pending flag is held in EIP-1153 transient storage so it cannot persist across unrelated calls.
  • UpgradeGate occupies the first 50 storage slots of both contracts. Nothing may be inserted above Admin.pendingOps; the layout is pinned by AdminFlashUUPS.t.sol.

Residual, stated in the source: Pool.flashSend books no repayment obligation, so the only enforcement of a flash repayment is the balance check inside Flash.flashLoan. Proxying puts that check behind an upgrade. The same authority already swaps Pool itself through the fleet upgrade under the same GOVERNANCE-tier timelock, so this grants no capability it did not already hold.

3.3. Mark store (moves with the Pool implementation)

The chain’s mark store lives in the Pool implementation’s storage and runs the logic that implementation names (LOGIC, MarkStoreP8 at launch). There is no oracle proxy and no oracle beacon: the store changes only through §3.1, where migrateFrom copies it into the new implementation. A P8 source is copied raw; the new class table may add or retune classes but must class every registered lane, and stored σ reads clamp to the new floor. migrateFrom is factory-only and needs an empty target, and it carries no queued store op. Store governance (setAuth, register, reanchor, halt, unhalt) reverts on any implementation that is not the factory’s live one, so nothing lands on or queues against a retired or pending copy.

3.4. Contracts with no upgrade path

PoolFactory, the LPToken implementation and the linked libraries have no upgrade path at all: none of them sits behind a proxy. They are replaced only by deploying new instances and re-pointing what references them: a new Pool implementation plus a fleet upgrade (§3.1) for the libraries and the receipt implementation.

A pool cannot be re-pointed at a different Admin or Flash. Those addresses are Pool immutables, they live in implementation code rather than proxy storage, and PoolFactory.requestReferenceUpgrade requires a candidate implementation to carry byte-identical AC / admin / flash immutables (PoolFactory.sol). That is why both ship behind proxies from the first deploy.

4. Security through timelocks

Dangerous operations are tiered per the on-chain constants. Duration table: Access Control §3. Untimelocked emergency levers and their authority: Access Control §2.

4.1. Timelock mechanics

TimelockLib.sol is a three-function library (pack(delay, grace), validate(packed), isLive(packed)), not a queue contract. The queue lives on Admin: one generic request, one cancel, one named execute per op. The request side is plumbing (pick a tier, key it, store the blob, emit) and is shared; the execute side is not, since each op has its own decode shape, validation, pool setter and event, so it stays named and individually testable. There is no generic OperationExecuted event.

admin.requestOp(pool, opType, subject, payload); // emits TimelockRequested(pool, key, opType, executableAt) // wait the tier delay admin.executeAddAsset(pool, token); // emits the operation's own event, e.g. AssetAdded // or, any time before execution: owner OR any guardian on a pool with no seat, only the seat on a seated pool admin.cancelOp(pool, opType, subject); // emits TimelockCancelled(pool, key, opType)

opType is the IPool.OpType ordinal; declaration order and the op→tier map are in Access Control §4. subject (token, curve id, roster hash, or ignored for pool-wide ops) and the payloads: Admin Contract §4.1.

Request and cancel share one key derivation, so every queueable op is cancellable without two functions having to agree on a key.

A live pending op cannot be silently re-queued: requestOp reverts AlreadyPending while the key holds an entry inside eta + GRACE_PERIOD_SECS. Cancel first, then re-request. Otherwise a payload swap plus an eta reset would restart the LP exit-notice clock unobserved.

An expired entry is overwritten rather than refused. Past eta + GRACE_PERIOD_SECS an op can never execute (TimelockLib.validate reverts Expired), so it is no one’s notice period. It is a dead key that blocks its own lane, and nothing on chain enumerates which keys are in that state. A fresh request on such a key emits TimelockCancelled, then TimelockRequested with a full fresh delay. No clock is shortened, and recovery costs one transaction instead of two.

The predicate is TimelockLib.isLive, shared by Admin.requestOp (which requestAssetParams routes through) and UpgradeGate.requestUpgrade; PoolFactory.requestReferenceUpgrade spells the test out inline because upgradeTimelock is a raw eta. The mark store keeps its own queue: the first owner call queues the calldata hash, a repeat inside the grace window executes it (MarkStoreBase._timelocked).

Event signatures: TimelockRequested(address indexed pool, bytes32 indexed id, uint8 opType, uint48 executableAt) and TimelockCancelled(address indexed pool, bytes32 indexed id, uint8 opType) (IAdmin.sol). Execution emits the operation-specific event, never a generic one.

Ops auto-expire after eta + grace. PoolFactory.executeReferenceUpgrade is no exception: it reverts Err.Expired past upgradeTimelock + SC.GRACE_PERIOD_SECS (§3.1).

5. Mainnet configuration

  • AccessControl owner: a governance multisig whose threshold satisfies QuorumLib.admin(n) = ceil(2n/3) over n in [3, 16], so 2-of-3, 3-of-4 or 4-of-5. A 3-of-5 is rejected: armQuorumPolicy() reverts Err.ThresholdViolation(3, 4). This is the owner resolved by every onlyAdmin check across the DEX: Admin, PoolFactory, the mark store, and every Pool, official or not. Arming preflight: Access Control §1.1.
  • Treasury: AccessControl.treasury() is a pointer, not a principal. The owner rotates it at the GOVERNANCE tier, guardian-cancellable while pending. Custody detail: Treasury.
  • Pool deployment is owner-only: PoolFactory.createPool is onlyAdmin, so the single AC owner mints every pool and names its admin seat in initdata (seat 0 resolves back to the owner). The governance lane (fleet upgrade, fences, un-halt on an official pool) stays with the AC owner whatever that seat holds.

6. Best practices

6.1. For pool operators

  1. Use a multisig for the AccessControl owner, with a threshold satisfying ceil(2n/3): 2-of-3, 3-of-4, 4-of-5.
  2. Monitor TimelockRequested and TimelockCancelled on Admin for your pool, plus the per-operation execution events (AssetAdded, RiskConfigUpdated, AnchorUpdated, CurveUpdated, FeeParamsUpdated, TreasuryUpdated, BaseTokenMigrated). There is no generic OperationExecuted.

6.2. For users

  1. Verify pool address and admin. Query factory.isOfficialPool(pool) to distinguish official from unbranded pools; the governance lane is the same single owner either way (§5).
  2. Monitor timelock events. Subscribe to TimelockRequested for your pool, review pending ops, exit if suspicious.
  3. The upgrade model:
    • Your pool’s code is not immutable. It is a proxy the factory upgrades: one owner transaction at the factory, after the GOVERNANCE delay, changes the code of every live pool including yours. Watch ReferencePoolUpgradeRequested on PoolFactory for the full notice window (§3.1).
    • A queued upgrade can be vetoed by the owner or any guardian, and expires 7 days after maturity if not executed.
    • Admin and Flash are themselves upgradeable behind proxies, under the same GOVERNANCE delay; watch UpgradeRequested on both.

7. Emergency procedures

Authority, delays and the full lever list: Access Control §2. The two calls this page owns:

factory.cancelReferenceUpgrade(); // veto a queued fleet swap: owner or any guardian admin.pause(); // freeze a matured UUPS request without cancelling it