Security Overview

The contract-level view of AIMM security: the adversaries the design is built against, the four layers that answer them, how storage and reentrancy are isolated, and which limitations are deliberate rather than pending. Authority, who may halt, who may upgrade, and how long each takes, is Access Control, Roles & Emergency Powers. Disclosure intake is Bug Bounty. Operational checklists live with their audience: Pool Deployment & Curation §7 for deployers and curators, Providing Liquidity §10 for LPs. A taker holds none: the slippage bound, the TTL staleness gate and the coverage toll are enforced on the swap path by the contracts, and the one caller-supplied input is minAmountOut (Basic Operations, Quotes & Routing).


1. Defense layers

LayerFocus
EconomicExternal mark, inventory skew bounds, coverage toll
Access controlTimelocked governance, asymmetric guardian halt
OperationalAsset halt, depeg halt, flow guards
CodeTests, internal review, invariant proofs; third-party audit pending; §10

2. Threat model

2.1. Adversary capabilities

AdversaryCapabilitiesMitigations
Flash Loan AttackerUnlimited capital for single transactionNo write-on-swap (external mark); volatility-adaptive per-push deviation band
Whale TraderLarge positions, multi-block attacksExternal keeper mark; inventory skew bounds
MEV SearcherOrdering, sandwich; oracle-push OEVSlippage protection; symmetric spread + skewed mid; minFee; private keeper relay preferred
Oracle ManipulatorCompromised keeper; stale pushk-of-n quorum + revokeSigner; deviation band; confidence halt; depeg bands
Governance AttackerMajority controlTimelocks + grace windows (Access Control & Roles)
Smart Contract ExploiterCode bugsGuardian halt; beacon upgrade is fleet-wide after the UPGRADE tier delay with a 7-day grace, cancellable (Deployment & Upgrades)

2.2. Trust assumptions

Only one principal here can do something destructive. The rest are bounded by what the contracts let them call, not by an expectation that they behave.

PrincipalTrust levelWhat it can actually do
OwnerTrustedThe one destructive authority. Writes risk parameters, curves, oracle config and hooks, upgrades the fleet, and is the only principal that can un-halt. Upgrades are timelocked; risk params deliberately are not (3.2 §7.3)
GuardianLimited, fail-safeHalt, tighten, cancel, and nothing else. Cannot un-halt, cannot upgrade, cannot move value. Compromising it degrades the protocol to a stopped state, never a drained one (3.1 §4)
TreasuryNot trusted with user fundscollectProtocolFees and nothing else (Admin.sol:342): it pulls the protocol’s own accrued fee share to itself. It cannot touch reserves, liabilities, LP positions or parameters. A compromised treasury address costs the protocol its fees; it does not put a depositor at risk
Treasury ownerTrusted, scoped to custodyRotates the treasury address, through the queue-then-execute timelock. A second governance authority on purpose, so fee custody can sit on a different multisig from the parameter owner, and either can veto a pending rotation
NXR signersVerified, k-of-nThe mark’s actual authority. A price is accepted because at least k of n registered signers signed that exact blob, never because of who submitted it
Oracle relayer (keeper)UntrustedbatchPushSigned takes no sender check (ExternalOracle.sol:507), so anyone may submit a validly-signed blob and the keeper holds no signing key. It can delay, withhold or reorder: a liveness actor, not a trust one, and staleness is what the chain gates on
Pool deployerUntrustedcreatePool is permissionless. The deployer’s address seasons the CREATE2 salt and confers no authority (Pool Deployment & Curation §2)
UsersUntrustedEvery input validated at the boundary

3. Security layers

3.1. Layer 1: economic security

Manipulation resistance:

  • External keeper mark: quote source is an off-venue aggregate (NX-Rates), not pool reserve state; reserve moves cannot move the quote.
  • No write-on-swap: a swap never mutates a feed, so there is no accumulator to manipulate.
  • Inventory skew bounds: Pricing.computeInventorySkew returns a dimensionless int8 clamped to [-100,100], saturating at coverage c12 and c2. One skew unit maps to 104/200=50 bps of curve-x displacement, so the clamp caps the coverage-driven mid shift at ±5000 bps of the curve, then clamps into range. Spline depth and spread are separate terms, so this is not a cap on total price impact. The bounds are fixed in code, not operator-set.
  • Reserve floor: the per-asset minLiquidity is the only hard outflow gate: a swap, withdrawal or flash loan reverts InsufficientAmount if it would leave Rliq below it (PoolIOLib.settle; there is no exec function). There is no coverage-ratio drainage floor: coverage prices flow, it never blocks it.

Incentive alignment:

  • Coverage-aware pricing: the symmetric spread plus inventory-skew mid shift and convex coverage toll make coverage-worsening flow pay more (no directional fee term in the spread itself).
  • LP haircuts: an exit from an under-covered leg is haircut linearly in the deficit, and the suppression cannot be configured away (haircutSuppressorBps < 20_000 enforced at the write, == 0 forced on a coverage-walled leg). It fires at the moment an LP actually leaves, so leaving raises the coverage ratio for those who stay and there is no first-mover advantage. Coverage is never rewritten by a background process.

3.2. Layer 2: access control

Four principals, owner, treasuryOwner, guardians and risk stewards, plus the oracle signer set, with seven timelock tiers from 1 to 14 days under PROD_DELAYS. Full principal table, guardian can/cannot matrix, halt authority and the duration schedule: Access Control, Roles & Emergency Powers (SSoT).

Two properties are worth stating here because they shape everything else:

  • Guardians are fail-closed only: halt, tighten, cancel; never un-halt, widen, or write params.
  • Risk parameters are deliberately not timelocked: bounded instead by owner fences plus a relative step clamp (Deployment & Upgrades §7.3).

3.3. Layer 3: operational security

Per-asset feature flags (PoolConstantsLib.sol):

HALT_RISK_BIT = 1 << 0 // Per-asset risk halt SWAP_ENABLED_BIT = 1 << 1 // Swap operations LIABILITY_SWAP_ENABLED_BIT = 1 << 2 // LP position swaps FLASH_ENABLED_BIT = 1 << 4 // Flash loans HALT_GUARDIAN_BIT = 1 << 6 // Guardian emergency halt (separate source from risk) // bits 5 and 7 are reserved; bit 3 is unallocated HALT_MASK = HALT_RISK_BIT | HALT_GUARDIAN_BIT // checked at every value-moving gate

Halt granularity is per-asset: there is no pool-wide pause bit, and HALT_MASK gates deposit, donate, withdrawTo and swapLiability as well as swaps. Who may set and clear each bit, and every other untimelocked lever: Access Control & Roles §4.

The operator-set thresholds are four, and only four:

ThresholdBound
minLiquidityper-asset reserve floor, the hard outflow gate
haircutSuppressorBpshow much of an under-covered leg’s deficit a same-asset exit is spared; strictly below 20,000, so the haircut can never be disabled
kappaCovBpsconvex coverage-wall strength; 0 disables the wall and is forbidden on every listed asset including the hub
refBandBpsfeed-relative depeg tolerance, mandatory on every non-base leg (Depeg Halt §2.4)

3.4. Layer 4: code security

Solidity =0.8.36 (exact pragma), custom error types, and the build-time artifact guards that pin storage layout (ArtifactGuards.t.sol, AdminFlashUUPS.t.sol). The test suite covers unit, integration, fuzz and invariant cases; specific proofs are cited at the property they establish rather than claimed in aggregate here.

Third-party audits: none published, and no pre-launch external review completed. See §10.


4. Storage security

4.1. Standalone-contract storage isolation

Storage isolation is structural: every contract is standalone with its own default storage layout, and cross-contract calls between distinct singletons (AdminPool, FlashPool) are standard external calls, no shared storage, so slot collisions between different contracts are impossible by construction. Pool’s own internal DELEGATECALL targets, the linked libraries PoolConfig, PoolLiquidity, Pricing and NUQuartic (see AIMM Overview §2.1 for the no-Diamond architecture), are a separate case: they take Pool’s $ as a storage parameter, so the compiler resolves the slots and there is no hand-mirrored layout to drift.

  • Each Pool is an ERC-1967 beacon proxy on PoolFactory (the factory is the beacon) with its own PoolStorage at slot 0. Cross-pool storage isolation is automatic; code is shared and swappable fleet-wide.
  • The Admin and Flash singletons are UUPS contracts behind ERC-1967 proxies (UpgradeGate, placed first so its 50 reserved slots lead the layout); their state is keyed by (pool, ...).
  • The reference Pool impl follows an append-only rule on PoolStorage: existing fields’ offsets and types are frozen across upgrades (new fields appended only). ArtifactGuards.t.sol pins it at build time, asserting Pool declares exactly one storage entry ($ at slot 0).
  • ERC-7201 namespaced storage is not used anywhere: plain default storage at slot 0 is sufficient since DELEGATECALL targets are fixed at compile time (no Diamond/module-registry pattern).

4.2. Transient storage (EIP-1153)

Transaction-scoped only, cleared automatically at the end of the transaction: reentrancy guards, oracle price caching, flash-loan state.


5. Reentrancy protection

Pool inherits TransientGuard (shared/evm/src/base/TransientGuard.sol), which overrides Solady’s mainnet-only default so TSTORE/TLOAD is forced on every chain, and marks every external entrypoint nonReentrant: the mutex slot is Solady’s, not a hand-rolled one. A second, slot-isolated transient flag blocks reserve-crediting inflows while a flash callback is in flight. Both guards, the exploit they close and their gas: Flow Guards.


6. Oracle security

AttackDefense
Flash loanNo write-on-swap (quote source is an external mark, not pool state)
Multi-blockVolatility-adaptive per-push deviation band + reference-feed halt, independent where the reference carries a disjoint signer set (3.6 §4.6)
Low liquidityN/A, price is an external keeper mark, not pool-liquidity-derived
StalenessTTL-based freshness check (fail-closed) + staleness surcharge

Validation on the push path, all on-chain and all fail-closed:

  • A per-feed TTL revert.
  • A mandatory per-feed push clamp (maxDeviation, enforced in _checkDeviation; maxDeviation == 0 reverts at addFeed and updateFeed), volatility-adaptive and hard-capped, so a compromised signer quorum is bounded to a monitorable step per push rather than a one-shot move.
  • A k-of-n distinct-signer quorum per batch.

Multi-source aggregation happens off-chain in the keeper (NX-Rates); the chain sees a single mark. Formula and terms: Oracles §8.3. Quorum ceremony: 3.6 §4.

There is one mode and no fallback switch. Degradation is a ladder of reverts:

ConditionEffect
age past half the TTLstaleness surcharge widens the spread
age past the TTLrevert StaleData (fail-closed)
confidence above MAX_CONFIDENCE_HALT_BPS (1,000 bps, strict)revert ThresholdViolation
base-token depeg past BASE_DEPEG_HALT_BPSrevert BaseDepegged (hub halt)
spoke mark outside refBandBps of its referencerevert PriceOutsideRefBand

Thresholds and readers: Oracles §8.2, Depeg Halt §2.


7. Upgrade security

Pools are not per-instance immutable. Upgrades happen by swapping the implementation on the shared beacon at PoolFactory, which re-points every live pool at once, third-party pools included, with no opt-out and no version pinning. Admin and Flash upgrade separately as UUPS singletons under the same UPGRADE tier. Procedure, grace window, cancel authority and the storage-layout obligation: Deployment & Upgrades §4.

PoolFactory, the LPToken implementation, ExternalOracle and the four linked libraries have no upgrade path at all: none sits behind a proxy. Replacing any of them is a redeployment plus a repoint, which for an oracle means a per-asset UPDATE_ORACLE op at the BASE tier (Deployment & Upgrades §4.3).


8. Emergency procedures

Runbook and authority: Access Control, Roles & Emergency Powers §4. Disclosure intake: Bug Bounty.


9. Known limitations

9.1. Economic bounds

LimitationBoundImplication
Max skew±100 (dimensionless)the skew offset saturates at the band edge; sign only, no separate premium parameter; see §3.1 for the mapping to curve bps
Anchor depthMAX_DEPTH = 4 (enforced, general)Each asset may anchor to any non-base parent within 4 steps and price against that parent’s mark - the tree is general, not fixed at one level. Whether a given pool uses that depth is configuration: there is no feature flag, depth is whatever the anchor column says, and a deep edge is one timelocked UPDATE_ANCHOR op away once its cross mark exists
Max pathLmax=2Dmax+1=9 nodes (8 legs)Unique tree path via the LCA; only the two endpoints settle. Depth bounds one walk, a path is two
Worst-case trade costNot bounded by config, by design. A spread widened by σ, confidence or staleness is the honest price of that risk; capping it would sell an underpriced quote and cap the pool’s defense exactly when it is most needed. The bound is the caller’s minAmountOut, exact and per tradeAn integrator must quote and enforce, not read a ceiling off config. The uint16 saturation of SwapQuote.spreadPbps is a field width, not a policy

9.2. Oracle limitations

The mark is an off-chain aggregate, signed by a k-of-n attester set and relayed by an unpermissioned submitter. That is three places a price can be attacked, and they are not interchangeable: the on-chain guards answer the second and third well and the first barely at all. Push-path mechanics: 3.6 §5.

SurfaceAttacker must controlWhat the contracts doResidual
1. The source. The aggregate is computed faithfully; its inputs are notA majority of the volume-weighted venues behind one NX Rates composite, simultaneously, for at least one push intervalNothing that can detect it. The signature covers a correctly computed aggregate of manipulated inputs, so every check passes by construction. FeedMathLib.deviationBand bounds how fast the mark may move; PoolIOLib.priceBandGuard compares it against a reference built from the same aggregationThe largest of the three. Bounded economically and off-chain, never cryptographically
2. The price provider. A signed price that existed on no venuek of n granted attester keys (k=2, n=3 on current deployments), or coercion of the replicas holding themNxrSignerSet._verifyQuorum: k distinct granted signers over one verifyingContract-bound EIP-712 digest. Magnitude is then clamped per push by FeedMathLib.deviationBand, σ is floored at the realized |Δp|/p, confidenceBps > MAX_CONFIDENCE_HALT_BPS (1,000) fails closed, sourceTs must strictly advance, and a sourceTs more than SOURCE_TS_FUTURE_SKEW_SECS = 5 ahead is rejected. Guardian revokeSigner and pauseFeed are immediateThe band caps the step, not the sum. The cumulative bound is the reference band, and it collapses wherever the reference shares the primary’s signer set, Arc today
3. The keeper. Withhold, delay, reorder, submit selectivelyThe keeper host and its funded EOA. No signing key: push authority comes from the signatures, msg.sender is unpermissionedIt cannot forge or edit: the digest commits to the whole blob. Reorder and replay die on the monotonic sourceTs (V1 reverts; V2 skips the record; the live V4 steps over the whole slot, silently, and its guard is one accepted write per slot per source second, not one per feed per block). Relabeling an old blob as fresh dies on FeedMathLib.obsAt =min( attested sourceTsMs, landing updatedAtSecs ) — on V4 those are the same number, and the defense is instead the 6 h MAX_RECON_AGE acceptance window, applied on the read side as well. V4 has no maxRelayLagSecs. Withholding hits the TTL and fails closedWithholding is a halt, and a halt is still a win for a griefer. Delay inside the premium-free grace, min(ttlSecs/2,30s), is free

Surface 1 has no cryptographic answer, by construction. What exists is off-chain and statistical: NX Rates refuses to sign a composite that carries fewer than min_accepted_providers accepted venues (2 on mainnet), too few genuinely-ticking legs, or a composite uncertainty above its class ceiling (20 bps pegged, 150 bps volatile); per-source weight is capped at clamp(HHIwinsorized,·) so no single venue carries the mark. Every one of those catches a minority venue moving: dispersion widens the composite CI and the quote is refused. A coordinated majority move does the opposite: the legs agree, the CI stays tight, and the composite is signable and correctly signed. Downstream, the deviation band bounds how fast that mark can walk the pool and the reference band adds nothing at all, because the reference is built from the same aggregation. Stated precisely: both bound the rate of change, neither bounds correctness. The real defense is the capital cost of moving a majority of the weighted book, plus monitoring, and for a thin asset that cost is low, which is why listing is a curation decision (Pool Deployment & Curation §7).

A stale or off-market price is refused before it is signed, off-chain, not on-chain. Each NX Rates replica countersigns a peer-proposed blob only after re-validating every record against its own live market view:

  • Price within the feed’s cosign_tolerance_bps (per-feed, bounded to [0.01,5] bps).
  • sourceTs skew and age bounds.
  • Agreement with its own provider-observation timestamp.
  • σ within an understatement floor and an overstatement ceiling.
  • Confidence below the on-chain halt threshold.

Below quorum nothing is served, so one compromised or lagging replica cannot get a bad price signed. What the chain cannot do is verify that this happened: on-chain, a batch is a valid k-of-n signature and a TTL, nothing more. Read the co-signing check as the defense against a minority compromise, and the guards in surface 2 as the defense against the quorum itself.

Where the reference oracle shares the primary’s signer set, surface 2’s cumulative bound is gone. One compromised quorum signs the mark and the reference together, the refBand walks in lockstep and never trips, and only the per-push rate limit and the guardian levers survive. This is a per-chain deploy property, not a protocol property: verify it by comparing SignerGranted logs on the two oracle addresses. Arc shares a set today (3.6 §4.6).

The keeper’s lever is choice, not content. The blob is public and batchPushSigned takes its authority from the signatures, so anyone holding a valid blob can land it: that bounds censorship, not timing. Within the TTL a relayer still chooses which fresh blob lands and when, and up to the grace window the quote carries no staleness premium, so a small deliberate delay costs it nothing; that is the same push-ordering OEV a public mempool grants any observer, and it is why the keeper prefers a private relay (§2.1). There is no on-chain mitigation for it. It is bounded operationally:

  • Leader election with failover across a configured keeper set.
  • A per-feed staleness alarm at max(2·heartbeat,ttlSecs/2).
  • A periodic manifest-parity check against the NX Rates roster.

Availability is a real limitation, but it is not the manipulation model. Cadence and outage bound how well the pool tracks the market and when it stops trading; they are not paths to a wrong price.

ConditionWhat happens
Low keeper cadence, stale markPriced first, then refused: the spread carries a staleness premium growing with στ past a grace of min(ttlSecs/2,30s), and past the feed’s TTL FeedMathLib.gate reverts StaleData. Shorter TTLs for faster feeds
Fast price movementWider spreads via σ. The per-push band widens with the attested Δt, so a genuine gap move ladders in rather than wedging the feed; past the 10dmax ceiling it does wedge, and the release is the owner’s timelocked requestFeedWidenexecuteFeedWiden, shipping in the next release; until then, an oracle redeploy plus a BASE-tier UPDATE_ORACLE repoint per leg (3.6 §5.1)
Keeper or feed outageFail-closed: swaps revert past TTL, halt beating bleed. Multi-venue aggregation removes the single-venue outage, not the single-provider one
Reference feed parkedEvery armed spoke band fails closed once the reference passes its own TTL, so the reference must be relayed on the same discipline as the primary

10. Audit status

Third-party audit is pending. Until those reports are published, treat the contracts as experimental: the assurance behind them today is the internal review, the test suite and the invariant proofs described on this page, not an independent opinion. Reports will be linked here when they land; there is no placeholder page in the meantime. See Risk Disclaimer.


PageRole
Access Control, Roles & Emergency PowersPrincipals, guardian surface, halt authority, timelock table (SSoT)
Bug BountyDisclosure intake
Deployment & UpgradesBeacon + UUPS
Flow GuardsReentrancy + deposit→withdraw cooldown
OraclesExternal mark, TTL, bands
Depeg HaltBase + spoke circuit breakers
Oracle Price-PushSigner quorum, push guards
ObservabilityHealth metrics, tradableRatio, ingest gates
Guardian OperationsRunbook for the guardian key: levers, argument traps, escalation
Oracle Keeper OperationsRunbook for the push relay: triggers, cadence, revert taxonomy
Risk Steward OperationsRunbook for the fenced param lane: clamps, fences, what fails closed
AdminPer-pool admin ops
Risk DisclaimerLegal risk surface