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:
requestReferenceUpgraderequires the candidate to be a contract carrying the sameAC/admin/flashimmutables as the live impl (PoolFactory.sol). 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). UnderConstants.PROD_DELAYSthat is 7 days. - Grace window: the matured request expires
SC.GRACE_PERIOD(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
isGuardianaddress viacancelReferenceUpgrade()(PoolFactory.sol). Residual, stated in the source: a fully compromised owner cansetGuardian(false)and re-request, so the guardian veto raises the bar rather than being absolute. - Monitor
ReferencePoolUpgradeRequestedandReferencePoolUpgradedonPoolFactory. The beacon address isPoolFactory.beacon(), the factory itself. PoolStoragefield 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()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 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 executePools, 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, whichsetAssetParamsalso routes through;ExternalOracleV4.requestFeedWiden;- both
NxrSignerSetqueues; UpgradeGate.requestUpgrade;PoolFactory.requestReferenceUpgrade, which spells the same test out inline because itsupgradeTimelockis 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)overnin[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()revertsErr.ThresholdViolation(3, 4). This is the owner resolved by everyonlyAdmincheck across the DEX:Admin,PoolFactory,ExternalOracle, and everyPool, official or permissionless. 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 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
- 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 permissionless pools; admin authority 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 beacon proxy: 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 beacon proxy: 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