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, whoseupgradeToanswers the factory only.executeReferenceUpgrade()first callsmigrateFrom(oldImpl)on the new implementation, which takes the chain’s mark store (it lives in the implementation) before any pool reads it, then callsupgradeToon 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:
requestReferenceUpgraderequires the candidate to be a contract carrying the sameAC/admin/flashimmutables as the live impl, astorageVersionat or above the live one, and a mark-store logic whoseFACTORYis this factory (PoolFactory.sol). The store logic’s ownACis not pinned; the owner checks it before requesting. Storage-layout compatibility is not on-chain-checkable and is pinned at build time byArtifactGuards.t.sol, which assertsPooldeclares exactly one storage entry ($at slot 0). - Delay: the
GOVERNANCEtier ofAccessControl.GOV_DELAYS(), read once into the factory’sDELAY_UPGRADEimmutable at construction (PoolFactory.sol). UnderConstantsLib.PROD_DELAYSthat is 7 days. - Grace window: the matured request expires
SC.GRACE_PERIOD_SECS(7 days) after its eta and then revertsErr.Expired(PoolFactory.sol). A stale request must be re-requested, with nocancelReferenceUpgradefirst: an expiredpendingReferencePoolis overwritten in place, announced asReferencePoolUpgradeCancelledthenReferencePoolUpgradeRequested, at the fullGOVERNANCEdelay (§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
ReferencePoolUpgradeRequestedandReferencePoolUpgradedonPoolFactory, andMigratedon the new implementation. PoolStoragefield 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()forAdminandFlash. - The candidate implementation must resolve to the same governance root.
UpgradeGate._pinGovernanceRootcompares itsACagainst the live one and reverts atrequestUpgradeand again atexecuteUpgrade, so an AC-mismatched implementation never reaches the proxy. Asymmetric with §3.1: the gate pinsAConly, wherePoolFactory._validateImplementationpinsAC,adminandflash. - The upgrade-pending flag is held in EIP-1153 transient storage so it cannot persist across unrelated calls.
UpgradeGateoccupies the first 50 storage slots of both contracts. Nothing may be inserted aboveAdmin.pendingOps; the layout is pinned byAdminFlashUUPS.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)overnin[3, 16], so 2-of-3, 3-of-4 or 4-of-5. A 3-of-5 is rejected:armQuorumPolicy()revertsErr.ThresholdViolation(3, 4). This is the owner resolved by everyonlyAdmincheck across the DEX:Admin,PoolFactory, the mark store, and everyPool, official or not. Arming preflight: Access Control §1.1. - Treasury:
AccessControl.treasury()is a pointer, not a principal. The owner rotates it at theGOVERNANCEtier, guardian-cancellable while pending. Custody detail: Treasury. - Pool deployment is owner-only:
PoolFactory.createPoolisonlyAdmin, so the single AC owner mints every pool and names its admin seat ininitdata(seat0resolves 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
- Use a multisig for the
AccessControlowner, with a threshold satisfyingceil(2n/3): 2-of-3, 3-of-4, 4-of-5. - Monitor
TimelockRequestedandTimelockCancelledonAdminfor your pool, plus the per-operation execution events (AssetAdded,RiskConfigUpdated,AnchorUpdated,CurveUpdated,FeeParamsUpdated,TreasuryUpdated,BaseTokenMigrated). There is no genericOperationExecuted.
6.2. For users
- 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). - Monitor timelock events. Subscribe to
TimelockRequestedfor your pool, review pending ops, exit if suspicious. - 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
GOVERNANCEdelay, changes the code of every live pool including yours. WatchReferencePoolUpgradeRequestedonPoolFactoryfor 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.
AdminandFlashare themselves upgradeable behind proxies, under the sameGOVERNANCEdelay; watchUpgradeRequestedon both.
- Your pool’s code is not immutable. It is a proxy the factory upgrades: one owner transaction at the factory, after the
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