Deployer

1. Overview

Per chain, the protocol deploys:

  1. The protocol AccessControl and two UUPS singletons behind ERC-1967 proxies (Admin, Flash). Order: script/DEPLOY.md.
  2. The mark-store logic MarkStoreP8 (class table from the chain manifest), the Pool reference implementation that runs it as LOGIC and holds the store’s storage, and PoolFactory. implementation() on the factory names the live Pool implementation; each proxy holds its own copy of that address. Protocol.s.sol then commits both oracle tiers’ rosters (setAuth), registers the lanes and reads them back.
  3. Five linked libraries, deployed by forge via CREATE2: PoolConfigLib, PoolLiquidityLib and PricingLib into Pool, NUQuarticLib into PoolConfigLib and PricingLib, AdminParamsLib into Admin (§4).
  4. Per-pool PoolProxy instances, deployed via the owner-only PoolFactory.createPool through the shared CREATE3 factory. Each proxy has its own storage and holds its implementation in its own ERC-1967 slot; its upgradeTo answers the factory only.

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.

Two upgrade paths, both operated by the owner multisig after handover (Admin):

  • Fleet upgrade at PoolFactory (GOVERNANCE tier delay): the new implementation takes the mark store (migrateFrom), then every pool proxy moves to it in the same transaction. The oracle moves only this way; each leg names a store lane in OracleConfig, and there is no per-leg repoint op.
  • UUPS on Admin and Flash, gated by the GOVERNANCE tier delay via requestUpgrade / executeUpgrade only.

The Router has no upgrade path: it holds no state and no funds between calls, so a new one is a new deployment (Composability §2).

Tier durations are deploy-time data, not constants of the design; the schedule is in Access Control §3. 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.

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.

2. Architecture

2.1. Core components

  1. Pool reference impl (Pool.sol)

    • Standalone AIMM contract. Holds the chain’s mark store in its own storage; pools read it through Pool.fallback, pushes and store governance delegatecall LOGIC. No internal TWAP.
    • Deployed once per protocol version; never called directly by users, except for mark pushes and store governance.
    • Each pool is a PoolProxy minted by the factory, initialised via initialize(...).
  2. PoolFactory.sol

    • Deploys PoolProxy instances (createPool(...)), holds the fleet-upgrade timelock (pendingReferencePool, upgradeTimelock), maps feed ids to store lanes (setFeedLane) and serves getFeed / feedOf.
    • Maintains allPools + officialPools + isPool registries.
  3. UUPS singletons, each an ERC-1967 proxy over an implementation carrying the immutable AC (AccessControl) ref:

    • Admin.sol, per-pool timelock queue + restricted setters. Holds real per-pool state (bootstrapSealed, riskFences, pendingOps), so redeploy-and-repoint is not 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 (onFlashLoan variant) flash-loan provider. Holds no persistent state; UpgradeGate’s 50 slots are the whole layout.
  4. AccessControl.sol, single owner source of truth, plus the treasury and factory pointers, the guardian set, and the packed governance delay schedule.

  5. Fee sink: each pool’s treasury() is a plain address, not a contract. See Treasury.

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 plain CREATE, so its address is f(factory, deployerKey, salt) and does not depend on the contract’s bytecode. Consequences: 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; and it survives a contract-generation change, since a new oracle version deploys to the reserved address for that role.

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, therefore a different factory, moving every address derived from it.

The deployer key is part of the address for singletons. The factory salts with keccak256(deployerEoa ++ salt), so the same salt signed by a different key lands at a different address with no revert. Mainnet addresses are mined for one specific key; signing a mainnet deploy with any other burns the reservation unrecoverably. Deploy scripts assert the signing key against the record before broadcasting, and the reserved mainnet addresses are published in Contract Addresses, so anything appearing at one of them ahead of a published deployment is not BTR.

Pools derive differently. A pool is minted through the same CREATE3 factory but with the PoolFactory as the CREATE3 sender, so the effective salt is keccak256(poolFactory ++ salt) and the address is f(create3Factory, poolFactory, salt). Three consequences: no deploy key is in a pool address, so a rotated deploy key does not move a pool reservation; the same salt lands on the same pool address on every chain of the fleet, block.chainid is not in the salt; and base token and token roster are mutable pool state rather than address inputs, so a base migration or a listing never moves a pool. PoolFactory.predictPool(salt) returns the address from the chain alone, no key and no salts file. Salts themselves are not public; the reserved pool addresses are published in Contract Addresses.

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: nothing to initialise, no uninitialised-proxy window.

One AccessControl per chain governs everything, the mark store included: the store logic takes the AC and the factory as constructor arguments, and both oracle tiers live in the one store under that root. Reference independence is a roster property, not a second governance root; see Mark Relayer.

3.2. Phase 2: deploy pool reference and factory

Phase 2 deploys the mark-store logic, the reference Pool implementation plus the PoolFactory, which records the fleet implementation. The Pool AC / Admin / Flash wiring is immutable and re-asserted on every fleet swap, with admin and flash the PROXY addresses, never the implementations. Its constructor also deploys the LPToken implementation the receipts clone.

3.3. Phase 3: deploy a pool

// Owner-only (`onlyAdmin`, the chain's AccessControl.owner()). address pool = factory.createPool( salt, // bytes32, the mined reservation; address = f(CREATE3_FACTORY(), factory, salt) baseToken, // anchor token (e.g. USDC), reverts ZeroAddress on 0 tokens, // address[] of assets to register on the pool, reverts InvalidInput when empty initdata // abi-encoded call forwarded to the pool (`initialize(baseToken, wnative, feeParams, poolAdmin_, lpTag)`); poolAdmin_ names the pool's admin seat, 0 resolves to AccessControl.owner(); lpTag is the bytes32 receipt tag ); // Pool is now an initialised PoolProxy on factory.implementation().

3.4. Phase 4: wire per-pool config

admin.requestOp(pool, uint8(IPool.OpType.ADD_ASSET), subject, payload); // AddAssetPayload carries lpSym, the bytes32 LP tag // wait the LISTING tier delay admin.executeAddAsset(pool, token); // execute paths stay named, one per op // The per-asset params are a separate owner call (instant pre-seal or on a tighten): admin.requestAssetParams(pool, token, minLiquidity, minFeePbps, vegaBps, coopDiscBps, coopRebateBps);

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

requestAssetParams applies immediately only before bootstrapSealed[pool] or on a defensive tighten; otherwise it queues itself at TUNING. Policy: Access Control §4.

4. 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: PoolConfigLib.sol, PoolLiquidityLib.sol, PricingLib.sol, NUQuarticLib.sol, AdminParamsLib.sol. Pool links the first three directly and Admin links the fifth; NUQuarticLib is linked into PoolConfigLib and PricingLib additionally into PoolLiquidityLib, so a link check that inspects only Pool reports three and misses two.
  • Inlined: AnchorTreeLib.sol, PoolIOLib.sol, PoolHooksLib.sol, PoolSolvencyLib.sol, YieldHookLib.sol, FeedMathLib.sol, MarkWordLib.sol, MarkP8Lib.sol, PoolConstantsLib.sol.
  • PoolFactory, the LPToken implementation, the mark-store logic (MarkStoreP8; it changes only with a new Pool implementation).
  • Shared ConstantsLib.sol, ErrLib.sol, TimelockLib.sol, AccessControl.sol.

5. 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 testnet and mainnet 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 defaulting. Production passes ConstantsLib.PROD_DELAYS; public testnets pass ConstantsLib.TESTNET_DELAYS. There is no throwaway zero-delay option: the deploy path rejects any schedule under ConstantsLib.MIN_ARMED_DELAY_SECS, so every deployed fleet arms its timelocks. Every delay quoted in these docs is the PROD_DELAYS value; a testnet fleet runs the shorter schedule for the same tier.

6. Owner-gated pool deployment

address myPool = factory.createPool(salt, baseToken, tokens, initdata); admin.requestOp(myPool, uint8(IPool.OpType.ADD_ASSET), subject, payload); // ... wait timelock, then execute (as the AC owner; the pool admin seat named // in initdata does not own the governance lane).
  • A white-label partner does not deploy. It requests a pool from the protocol owner, gets its seat named in initdata at creation, and administers that pool through Admin gated on Pool.poolAdmin(): listing assets, risk and oracle config, hooks, fee sink, sealBootstrap, halt. Fronting the pool with its own interface needs no permission.
  • 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, PoolConfigLib.sol); the modes are defined in Mark Relayer and the band in Flow Guards.

7. Best practices 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. Load-bearing, because a fleet upgrade 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 fleet upgrade 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 skips the real DELEGATECALL edge cases, since the linked-library DELEGATECALLs only trigger through a deployed proxy.

8. 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).