Deployer
1. Overview
Per chain, the protocol deploys:
- The protocol
AccessControland two UUPS singletons behind ERC-1967 proxies (Admin,Flash). Order:script/DEPLOY.md. - The mark-store logic
MarkStoreP8(class table from the chain manifest), thePoolreference implementation that runs it asLOGICand holds the store’s storage, andPoolFactory.implementation()on the factory names the live Pool implementation; each proxy holds its own copy of that address.Protocol.s.solthen commits both oracle tiers’ rosters (setAuth), registers the lanes and reads them back. - Five linked libraries, deployed by
forgevia CREATE2:PoolConfigLib,PoolLiquidityLibandPricingLibintoPool,NUQuarticLibintoPoolConfigLibandPricingLib,AdminParamsLibintoAdmin(§4). - Per-pool
PoolProxyinstances, deployed via the owner-onlyPoolFactory.createPoolthrough the shared CREATE3 factory. Each proxy has its own storage and holds its implementation in its own ERC-1967 slot; itsupgradeToanswers 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(GOVERNANCEtier 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 inOracleConfig, and there is no per-leg repoint op. - UUPS on
AdminandFlash, gated by theGOVERNANCEtier delay viarequestUpgrade/executeUpgradeonly.
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
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 delegatecallLOGIC. No internal TWAP. - Deployed once per protocol version; never called directly by users, except for mark pushes and store governance.
- Each pool is a
PoolProxyminted by the factory, initialised viainitialize(...).
- Standalone AIMM contract. Holds the chain’s mark store in its own storage; pools read it through
PoolFactory.sol- Deploys
PoolProxyinstances (createPool(...)), holds the fleet-upgrade timelock (pendingReferencePool,upgradeTimelock), maps feed ids to store lanes (setFeedLane) and servesgetFeed/feedOf. - Maintains
allPools+officialPools+isPoolregistries.
- Deploys
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 freshAdminwould arrive withbootstrapSealed == falsefleet-wide and re-open the untimelockedaddAsset/setCurvelanes 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.
AccessControl.sol, single owner source of truth, plus the treasury and factory pointers, the guardian set, and the packed governance delay schedule.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.Poollinks the first three directly andAdminlinks the fifth;NUQuarticLibis linked intoPoolConfigLibandPricingLibadditionally intoPoolLiquidityLib, so a link check that inspects onlyPoolreports three and misses two. - Inlined:
AnchorTreeLib.sol,PoolIOLib.sol,PoolHooksLib.sol,PoolSolvencyLib.sol,YieldHookLib.sol,FeedMathLib.sol,MarkWordLib.sol,MarkP8Lib.sol,PoolConstantsLib.sol. PoolFactory, theLPTokenimplementation, 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
initdataat creation, and administers that pool throughAdmingated onPool.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
- Storage layout is append-only:
PoolStoragefield 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. - No constructors with side effects.
Poolruns behind a proxy; onlyinitialize(...)runs per-instance. - 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.
- 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
PoolStoragelayout unchanged or safely extended (append-only).- No constructor side effects in the reference impl.
forge testgreen 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).