---
title: "Deployment & Upgradeability"
description: "Per-chain deploy order, the beacon and UUPS upgrade paths, and the timelock tiers that gate them."
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-04"
publish: true
---
# Deployment and upgradeability

## 1. Overview

The protocol deploys as, per chain:

1. The shared `AccessControl` singleton, one `ExternalOracle`, and two **UUPS singletons behind ERC-1967 proxies** (`Admin`, `Flash`).
2. The `Pool` reference implementation and `PoolFactory`. **The factory is the beacon**: `beacon()` returns `address(this)` and `implementation()` is the one slot every live pool reads (`PoolFactory.sol`).
3. Four **deployed, linked libraries** auto-deployed by `forge` via CREATE2 and linked into their callers: `PoolConfig`, `PoolLiquidity`, `Pricing`, `NUQuartic`. See [§5](#5-non-upgradeable-components).
4. **Per-pool ERC-1967 beacon proxies**, deployed via `PoolFactory.createPool` (`LibClone.deployDeterministicERC1967BeaconProxy`). Each proxy has its own storage and reads `PoolFactory.implementation()` for its code.

The `Pool` implementation constructor deploys the shared `LPToken` implementation that every per-leg receipt clones (`Pool.sol`), so it is not a separate deploy step.

Upgradeability is achieved via:
- **Beacon impl swap** at `PoolFactory` (`UPGRADE` tier delay), which re-points **every live pool at once**. See [§4.1](#41-pool-beacon-upgrade-poolfactory-7-day-timelock).
- **UUPS** on `Admin` and `Flash`, gated by the `UPGRADE` tier delay via `requestUpgrade` / `executeUpgrade` only ([§4.2](#42-uups-upgrades-admin--flash)). The `Router` is deployed but holds no state, no funds between calls and no upgrade path: it is immutable, and a new one is a new deployment (see [Composability §2](/docs/5-1-4-composability#2-routing-through-the-router)).

Tier durations are deploy-time data, not constants of the design; the schedule is in
[Access Control & Roles §5](/docs/3-1-access-control-roles-emergency-powers#5-timelock-parameters).
Every duration quoted on this page is the `PROD_DELAYS` value.

`Pool` bakes the `Admin` and `Flash` **proxy** addresses as immutables and `PoolFactory._validateImplementation` pins them on every fleet upgrade, so a live pool can never be re-pointed at a different governance or flash contract. Those two must be proxies from the first deploy or their logic is frozen for the fleet's life.

Cross-contract interactions *between distinct contracts* (Admin↔Pool, Factory↔Pool, Flash↔Pool) are normal external calls, not `delegatecall` - the DELEGATECALLs happen only *inside* a single `Pool`, to its linked libraries; see [AIMM Overview §2.1](/docs/1-overview#21-standalone-singletons--pool-proxies) for that architecture.

## 2. Architecture

### 2.1. Core components

1. **Pool reference impl** (`Pool.sol`)
   - Standalone AIMM contract. Reads the external-mark feed (`ExternalOracle`); no internal TWAP.
   - Deployed once per protocol version; never called directly by users.
   - Each pool is an ERC-1967 beacon proxy pointing at the factory, initialised via `initialize(...)`.

2. **PoolFactory** (`PoolFactory.sol`)
   - Deploys `Pool` beacon proxies (`createPool(...)`) and holds the beacon-swap timelock (`pendingReferencePool`, `upgradeTimelock`).
   - Maintains `allPools` + `officialPools` + `isPool` registries.

3. **UUPS singletons**, each an ERC-1967 proxy over an implementation that carries the immutable `AC` (AccessControl) ref:
   - `Admin.sol`, per-pool timelock queue + restricted setters. Holds real per-pool state (`bootstrapSealed`, `riskFences`, `pendingOps`), which is why redeploy-and-repoint is not an equivalent to an upgrade: a fresh `Admin` would arrive with `bootstrapSealed == false` fleet-wide and re-open the untimelocked `addAsset` / `setCurve` lanes on live sealed pools.
   - `Flash.sol`, ERC-3156-style (postFlashLoan variant) flash-loan provider. Holds no persistent state; `UpgradeGate`'s 50 slots are the whole layout.

4. **Shared**
   - `AccessControl.sol`, single owner source of truth, plus the independent `treasuryOwner` principal, the guardian set, and the packed governance delay schedule.

5. **Fee sink**: each pool's `treasury()` is a plain address, not a contract. `Pool.initialize` leaves it zero, and fee collection reverts until it is set; the pool-seed scripts wire it per chain through the `UPDATE_TREASURY` op (`HIGH` custody tier). Only that exact address may pull the pool's accrued protocol fees (`Admin.sol`, `collectProtocolFees`).

## 3. Deployment process

### 3.0. Phase 0: the CREATE3 factory, and why addresses are known before the deploy

Every singleton below is deployed through a CREATE3 factory rather than by plain `CREATE`, so its
address is `f(factory, deployerKey, salt)` and does not depend on the contract's bytecode. Three
things follow. The address is identical on every chain BTR launches on. It is known before the
contract is compiled, so keeper configs, monitoring and integration constants can be filled in
ahead of the ceremony instead of after it. And it survives a contract-generation change: a new
oracle version deploys to the reserved address for that role, not to a new one.

The factory itself is deployed once per chain from a **frozen** initcode artifact via the
canonical arachnid `CREATE2` proxy, never rebuilt from source - a rebuild under different compiler
settings produces different initcode and therefore a different factory, which would move every
address derived from it.

**The deployer key is part of the address.** The factory salts with
`keccak256(deployerEoa ++ salt)`, so the same salt signed by a different key lands somewhere else -
silently, with no revert. Mainnet addresses are mined for one specific key; signing a mainnet
deploy with any other burns the reservation unrecoverably. The deploy scripts therefore assert the
signing key against the record before broadcasting, and the reserved mainnet addresses are
published in [Contract Addresses](/docs/2-1-contract-addresses) so that anything appearing at one
of them ahead of a published deployment is recognisably not BTR.

### 3.1. Phase 1: deploy shared and singletons

Phase 1 deploys the shared `AccessControl` singleton - owner, treasury, and the packed governance-delay word passed at construction: `GOV_DELAYS` is read from the environment with no default, so an unset variable aborts the deploy - then the `Admin` and `Flash` implementations, each carrying the immutable AC ref, behind ERC-1967 proxies via `LibClone.deployERC1967`. Every mapping starts empty, so there is nothing to initialise and no uninitialised-proxy window.

A chain whose oracle stack shipped first reuses that chain's existing `AccessControl` rather than minting a second one: two ACs would split protocol governance and leave the oracle's guardian unable to halt the pools it feeds. `ExternalOracle` takes the AC address as a constructor argument, so this is a deploy-script decision, not a contract-enforced one. The one deliberate exception is a **reference** oracle instance, which should be governed separately from the primary it polices; see [Oracle Price-Push Security §4.6](/docs/3-6-oracle-price-push-security#46-independent-reference-the-deploy-disjointness-preflight).

### 3.2. Phase 2: deploy pool reference and factory

Phase 2 deploys the reference `Pool` implementation - its `AC` / `Admin` / `Flash` wiring is immutable and is re-asserted on every fleet swap, with `admin` and `flash` the PROXY addresses, never the implementations, and its constructor also deploys the `LPToken` implementation the receipts clone - plus the `PoolFactory`, which is itself the beacon holding the fleet implementation slot.

### 3.3. Phase 3: deploy a pool

```solidity
// Permissionless: any address may call this. Deployment ≠ administration -
// there is no `owner` param; every pool's admin functions resolve to the
// single protocol-wide AccessControl owner regardless of who deployed it.
address pool = factory.createPool(
    baseToken,          // anchor token (e.g. USDC)
    tokens,             // address[] of assets to register on the pool
    initdata             // abi-encoded call forwarded to the pool (typically `initialize(baseToken, wnative, feeParams)`)
);
// Pool is now an initialised ERC-1967 beacon proxy reading `factory.implementation()`.
```

### 3.4. Phase 4: wire per-pool config

```solidity
admin.requestOp(pool, uint8(IPool.OpType.ADD_ASSET), subject, payload);
// wait the LOW tier delay
admin.executeAddAsset(pool, token);   // execute paths stay named, one per op
// The four immediate per-asset params are a separate, untimelocked owner call:
admin.setAssetParams(pool, token, minLiquidity, minFeePbps, vegaBps, haircutSuppressorBps);
```

`decimals` is not an argument: it is read from the token at listing. Inventory skew takes no per-asset argument either; it is a fixed protocol law.

`setAssetParams` applies immediately only before `bootstrapSealed[pool]` or on a defensive tighten; otherwise it queues itself at `LOW`. Policy: [Access Control & Roles §6](/docs/3-1-access-control-roles-emergency-powers#6-owner-gated-op-classes).

## 4. Upgrade mechanisms

### 4.1. Pool beacon upgrade (PoolFactory, 7-day timelock)

Canonical description of the pool upgrade model.

```solidity
factory.requestReferenceUpgrade(address(newImpl));   // candidate carries the same AC / Admin / Flash immutables
// wait the UPGRADE 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();
```

**Key properties**:
- **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. There is no per-pool opt-out and 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 and to carry 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 `UPGRADE` 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 cannot be executed months later; it must be re-requested. Shipping in the next release, that re-request needs no `cancelReferenceUpgrade` first: an expired `pendingReferencePool` is overwritten in place, announced as `ReferencePoolUpgradeCancelled` then `ReferencePoolUpgradeRequested`, at the full `UPGRADE` delay ([§6.1](#61-timelock-mechanics)). This is the longest of the six tiers and therefore the likeliest to age out unnoticed.
- **Cancellable** by the owner or by 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()`, which is the factory itself.
- `PoolStorage` field order/types are **append-only** across versions, because live pools keep their storage across a beacon swap.

### 4.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.

```solidity
admin.requestUpgrade(newImpl);
// ... wait the UPGRADE 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. Note the asymmetry with §4.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 `UPGRADE`-tier timelock, so this grants no capability it did not already hold.

### 4.3. Contracts with no upgrade path

`PoolFactory`, the `LPToken` implementation, `ExternalOracle` and the four linked libraries have no upgrade path at all: none of them sits behind a proxy, and `ExternalOracle` states the property in-source as the reason its signer cap is 16 rather than 6. They are replaced only by deploying new instances and re-pointing what references them: a new `Pool` implementation plus a beacon swap ([§4.1](#41-pool-beacon-upgrade-poolfactory-7-day-timelock)) for the libraries and the receipt implementation, and a per-asset `UPDATE_ORACLE` op at the `BASE` tier for an oracle instance.

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.

## 5. Non-upgradeable components

A library is deployed and linked when it declares at least one `external` or `public` function; otherwise the compiler inlines it into its callers.

- Deployed, linked: `libraries/PoolConfig.sol`, `PoolLiquidity.sol`, `Pricing.sol`, `NUQuartic.sol`. `Pool` links the first three directly; `NUQuartic` is linked into `PoolConfig` and `Pricing` additionally into `PoolLiquidity`, so a link check that inspects only `Pool` reports three and misses one.
- Inlined: `AnchorTreeLib.sol`, `PoolIOLib.sol`, `PoolHooksLib.sol`, `FeedMathLib.sol`, `TransientCacheLib.sol`, `PoolConstantsLib.sol`.
- `PoolFactory`, the `LPToken` implementation, `ExternalOracle`.
- Shared `Constants.sol`, `Errors.sol`, `Timelock.sol`, `AccessControl.sol`.

## 6. Security through timelocks

All dangerous operations require time delays, tiered per the real on-chain constants - see [Access Control & Roles §5](/docs/3-1-access-control-roles-emergency-powers#5-timelock-parameters) for the full duration table, and [§4 there](/docs/3-1-access-control-roles-emergency-powers#4-halt-authority) for every untimelocked emergency lever and its authority.

### 6.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, and 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 (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.

```solidity
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 & Roles §6](/docs/3-1-access-control-roles-emergency-powers#6-owner-gated-op-classes). `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.

Because request and cancel share one key derivation, every op that can be queued can be cancelled by construction 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 different in kind, and **shipping in the next release** it 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: it is 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**, so no clock is shortened and recovery costs one transaction instead of two. The predicate is `TimelockLib.isLive`, and it is one definition across six queues: `Admin.requestOp` (which `setAssetParams` also routes through), `ExternalOracleV4.requestFeedWiden`, both `NxrSignerSet` queues, `UpgradeGate.requestUpgrade`, and `PoolFactory.requestReferenceUpgrade` — the last spells the same test out inline, because its `upgradeTimelock` is a raw eta with no packed op word. Every one 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.

Exact 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.

**Grace period**: ops auto-expire after `eta + grace`. `PoolFactory.referenceUpgrade` is no exception: it reverts `Err.Expired` past `upgradeTimelock + SC.GRACE_PERIOD` ([§4.1](#41-pool-beacon-upgrade-poolfactory-7-day-timelock)).

## 7. Deployment configurations

### 7.1. Mainnet

- **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). The full arming preflight is [Access Control & Roles §1.1](/docs/3-1-access-control-roles-emergency-powers#11-arming-the-quorum-policy).
- **Treasury owner**: `AccessControl.treasuryOwner()`, a **second, independent** governance principal with its own rotation and veto, gating fee custody so it can live on a different multisig from the param/admin owner. The two may be the same key but are not the same role. A pool's fee sink itself is a plain address, not a contract; only that address may pull the pool's accrued protocol fees.
- **Pool deployment**: permissionless - any address may call `PoolFactory.createPool`. Pool *administration* (add asset, halt, risk config, etc.) is not permissionless: it 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.

### 7.2. Governance delays are deploy-time data

The delay schedule is a **constructor argument**, not a chain-dependent branch. `AccessControl(owner, treasury, govDelays)` stores the packed word in the immutable `GOV_DELAYS`, and every governed contract derives its own tier delays from it at construction. No contract reads `block.chainid` to pick a delay, so a testnet and a mainnet deployment run identical code on different data.

Deploy scripts read `GOV_DELAYS` from the environment with **no default**: an unset variable aborts the deploy rather than silently picking a side. Production passes `Constants.PROD_DELAYS`; public testnets pass `Constants.TESTNET_DELAYS`. There is **no** throwaway zero-delay option: the deploy path rejects any schedule under `Constants.MIN_ARMED_DELAY`, so every deployed fleet arms its timelocks. Read a delay quoted anywhere in these docs as the `PROD_DELAYS` value; a testnet fleet runs the shorter schedule for the same tier.

### 7.3. Risk parameters are deliberately not timelocked

`setRiskFences` and `setAssetParamsBounded` bypass the timelock entirely. The steward lane writes exactly three fields (`minFeePbps`, `vegaBps`, `haircutSuppressorBps`) and never queues; `minLiquidity` is an argument only so the call can prove it is unchanged, and any delta reverts `InvalidInput`. The matching `RiskFences` are `minFeeHardMinPbps`, `minFeeHardMaxPbps`, `vegaHardMinBps`, `vegaHardMaxBps`, `haircutSuppressorHardMaxBps`, `haircutSuppressorHardMinBps` and `maxDeltaBps`: a hard min/max pair for each of the three fields, plus the one relative step bound.

This is not an oversight, it is a design boundary: BTR's edge is fast risk-parameter adaptivity, so those calls stay immediate, and the control is the bounded-delta guard plus a dedicated, revocable risk role, not a delay. The exemptions are narrower than "tightening is free"; see [Access Control & Roles §3](/docs/3-1-access-control-roles-emergency-powers#3-risk-steward) for which direction each field is clamped in and why the suppressor is never exempt. Structural changes (oracle mode, asset add/remove, curve preset repoint) still route through `requestOp` with `UPDATE_RISK` / `ADD_ASSET` / `UPDATE_PROFILE` and the full `LOW`-tier queue.

## 8. Permissionless pool deployment

```solidity
address myPool = factory.createPool(baseToken, tokens, initdata);
admin.requestOp(myPool, uint8(IPool.OpType.ADD_ASSET), subject, payload);
// ... wait timelock, then execute (as the AC owner - the deployer does NOT
// automatically gain admin rights over the pool they deployed).
```

- Timelocks are enforced on every pool, which protects pool users regardless of who administers it.
- Each asset picks its oracle mode at listing, and every non-base spoke carries a **mandatory** reference band (`requireExternalSpokeBound`, `PoolConfig.sol`); the modes are defined in [Oracles §1](/docs/3-4-oracles#1-overview) and the band in [Depeg Halt §2.4](/docs/3-5-depeg-halt#24-per-asset-price-band-depeg-guard-for-spokes).

## 9. Best practices

### 9.1. For pool reference-impl developers

1. **Storage layout is append-only.** `PoolStorage` field order and types are frozen across implementation versions. New fields appended only. This is load-bearing: a beacon swap moves every live pool onto the new code while keeping its existing storage.
2. **No constructors with side effects.** `Pool` runs behind a proxy; only `initialize(...)` runs per-instance.
3. **No new external dependencies without an explicit migration story.** A beacon swap migrates the whole fleet in one transaction, so a new dependency must be safe for every live pool simultaneously.
4. **Test the implementation AND a deployed proxy.** A proxy is what users hit; impl in isolation will skip the real DELEGATECALL edge cases - the linked-library DELEGATECALLs only trigger through a deployed proxy.

### 9.2. 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`, `OracleUpdated`, `CurveUpdated`, `FeeParamsUpdated`, `TreasuryUpdated`, `BaseTokenMigrated`). There is no generic `OperationExecuted`.

### 9.3. For users

1. **Verify pool address and admin.** Query `factory.isOfficialPool(pool)` to distinguish official vs. permissionless pools; admin authority is the same single owner either way (see [§7.1](#71-mainnet)).
2. **Monitor timelock events.** Subscribe to `TimelockRequested` for your pool; review pending ops; exit if suspicious.
3. **Understand the upgrade model.**
   - Your pool's code is **not** immutable. It is a beacon proxy: one owner transaction at the factory, after the `UPGRADE` delay, changes the code of every live pool including yours. Watch `ReferencePoolUpgradeRequested` on `PoolFactory` for the full notice window ([§4.1](#41-pool-beacon-upgrade-poolfactory-7-day-timelock)).
   - 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 `UPGRADE` delay; watch `UpgradeRequested` on both.

## 10. Emergency procedures

Authority, delays and the full lever list: [Access Control & Roles §4](/docs/3-1-access-control-roles-emergency-powers#4-halt-authority). The two calls this page owns:

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

## 11. Upgrade checklist

- [ ] `PoolStorage` layout unchanged or safely extended (append-only).
- [ ] No constructor side effects in the reference impl.
- [ ] `forge test` green against a deployed proxy, not the implementation alone.
- [ ] Fleet-wide blast radius reviewed: the swap re-points every live pool, including third-party ones.
- [ ] Timelock requested and announced; the delay window is the review period.
- [ ] Testnet swap executed and observed.
- [ ] ABIs regenerated and republished to the backend ABI endpoint (`GET /v1/abis/<ContractName>`); abi-freshness check green.
- [ ] Emergency halt paths verified.
- [ ] Monitoring + alerting wired to the new impl + factory.
- [ ] Cancel path tested (so the swap can be aborted if needed).
