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 both revert FeatureDisabled(TRANSFER) (AccessControl.sol). The only route to a new owner is Solady’s two-step handover with its 48 h request expiry. completeOwnershipHandover is overridden to reject address(0) and, once the quorum policy is armed (Access Control §1.1), to require the incoming owner to satisfy the same k-of-n admin policy. That override is the complete enforcement point for the owner principal.

2. The owner lane for asset params

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

The fee floor is the only fence binding the owner lane. setAssetParams calls _requireFeeFloor 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. Pool beacon 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 (7 days) factory.executeReferenceUpgrade(); // writes the factory's own `implementation` slot // OR cancel before exec: owner or any AC guardian. factory.cancelReferenceUpgrade();
  • Fleet-wide, not opt-in. Every deployed pool is an ERC-1967 beacon proxy reading PoolFactory.implementation(). executeReferenceUpgrade() writes that one slot, so a single owner transaction replaces the executable code of every live pool at once, including pools deployed permissionlessly by third parties. No per-pool opt-out, no version pinning. This is the protocol’s largest single trust assumption.
  • New-impl compatibility is asserted on request: requestReferenceUpgrade requires the candidate to be a contract carrying the same AC / admin / flash immutables as the live impl (PoolFactory.sol). 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 Constants.PROD_DELAYS that is 7 days.
  • Grace window: the matured request expires SC.GRACE_PERIOD (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 isGuardian address via cancelReferenceUpgrade() (PoolFactory.sol). Residual, stated in the source: a fully compromised owner can setGuardian(false) and re-request, so the guardian veto raises the bar rather than being absolute.
  • Monitor ReferencePoolUpgradeRequested and ReferencePoolUpgraded on PoolFactory. The beacon address is PoolFactory.beacon(), the factory itself.
  • PoolStorage field order/types are append-only across versions: live pools keep their storage across a beacon swap.

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 = 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 beacon under the same GOVERNANCE-tier timelock, so this grants no capability it did not already hold.

3.3. Oracle beacon upgrade (OracleBeacon, 1-day timelock)

The oracle is one OracleProxy per chain, an immutable address that delegatecalls OracleBeacon.implementation(). The gate lives in the beacon, not in the implementation, so a bad implementation cannot brick its own replacement.

beacon.requestUpgrade(newImpl); // owner; candidate must carry the same AC and storageVersion // wait the LISTING tier delay (1 day in production), then execute inside GRACE_PERIOD (7 days) beacon.executeUpgrade(); beacon.cancelUpgrade(); // owner or any AC guardian, any time before execute

Pools, foreign GEN-1 pools and external readers bind the proxy address forever. The Arc V4 pair predates the beacon; its V4→V5 migration is this upgrade, not a repoint.

3.4. Contracts with no upgrade path

PoolFactory, OracleBeacon, 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 beacon swap (§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

Timelock.sol is a two-function library (pack(delay, grace), validate(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, owner OR any guardian, any time before execution admin.cancelTimelock(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 is the third key component:

  • The left-padded asset address for token-keyed ops.
  • The preset id for UPDATE_CURVE.
  • Ignored for the three pool-wide ops.

Request and cancel share one key derivation, so every queueable op is cancellable by the shape of the code rather than by two functions agreeing.

A live pending op cannot be silently re-queued: requestOp reverts AlreadyPending while the key holds an entry inside eta + GRACE_PERIOD. 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 an op can never execute (TimelockLib.validate reverts Expired), so it is nobody’s notice period: a dead key holding its own lane shut, 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, one definition across six queues:

  • Admin.requestOp, which setAssetParams also routes through;
  • ExternalOracleV4.requestFeedWiden;
  • both NxrSignerSet queues;
  • UpgradeGate.requestUpgrade;
  • PoolFactory.requestReferenceUpgrade, which spells the same test out inline because its upgradeTimelock is a raw eta with no packed op word.

Each of those queues previously charged an extra cancel transaction to recover from its own dead entry, on levers whose purpose is to be reachable during an incident.

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.referenceUpgrade is no exception: it reverts Err.Expired past upgradeTimelock + SC.GRACE_PERIOD (§3.1).

5. Mainnet configuration

  • AccessControl owner: a governance multisig whose threshold satisfies Quorum.admin(n) = ceil(2n/3) over n in [3, 16], so 2-of-3, 3-of-4 or 4-of-5 (transitions to elected DAO council). 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, ExternalOracle, and every Pool, official or permissionless. 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 permissionless: any address may call PoolFactory.createPool. Pool administration (add asset, halt, risk config) is gated to the single AC owner for every pool, regardless of deployer. There is no per-pool owner or curator role in the deployed contracts.

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 permissionless pools; admin authority 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 beacon proxy: 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