> For the complete documentation index, see [llms.txt](https://docs.aegis.markets/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aegis.markets/part-4-system-structure/system-architecture-and-contract-roles.md).

# System Architecture and Contract Roles

This page describes the AEGIS Engine contract system from a structural standpoint — what the contracts are, how they're related, and what each one is responsible for. It is deliberately terse where the Statement of Intended Behavior (in the aegis-engine repo) is authoritative, and refers there for details.

### The three-layer model

┌─────────────────────┐

│     AegisRouterV1 (Periphery)                              │

│  • Batch orchestration                                         │

│  • Action routing                                                  │

│  • Pre-funding management                               │

└──────────┬──────────┘

&#x20;                                        │

┌──────────▼──────────┐

│      AegisEngine (Core Singleton)                        │

│  • Vault lifecycle (ERC-721)                                 │

│  • sL shares (ERC-6909)                                     │

│  • L-unit ledger                                                     │

│  • √K solvency checks                                         │

│  • Session management                                       │

└──────────┬──────────┘

&#x20;                                       │

┌──────────▼──────────┐

│    PoolManager (Uniswap V4)                             │

│  • AMM state                                                        │

│  • ERC-6909 token accounting                          │

│  • Delta settlement                                              │

└──────────┬──────────┘

Three invariants bridge the layers:

* Router orchestrates PM.unlock; engine orchestrates its own AE session nested inside.
* Engine never touches PM outside an unlock frame. Any such attempt reverts with ManagerLocked.
* Engine never holds ERC-20 balances at rest; all value sits either in the user's wallet or in the PoolManager's ERC-6909 accounting.

### Core contracts

#### AegisEngine (singleton)

The ledger. Owns:

* Markets (\_markets\[PoolId → Market.State]): per-pool state including totalRlBorrowed, borrowIndexWad, equityLWad, rateOracle, fullUtilizationRate, and the PoolKey.
* Vaults (\_vaults\[vaultId → Vault.State]): per-vault state including poolId, idleBalances\[currency], rL, and the set of attached positions\[].
* sL shares via inherited ERC6909ClaimsWithSupply — tokenId = PoolId, with per-tokenId totalSupply tracked.
* Session state in EIP-1153 transient storage: active vault count, per-vault session operator, pending NFT attach digest, cached TWAP, cached fee growth, accrual flags.
* Hook allow-map: hookAllowed\[IHooks → bool], one-way.

Key interfaces: IAegisEngine, IEngineStartCallback.

#### AegisRouterV1 (periphery)

The canonical periphery. Inherits BaseActionsRouter, DeltaResolver, ReentrancyLock, Permit2Forwarder, NativeWrapper. Responsibilities:

* execute(bytes unlockData, uint256 deadline) is the user entrypoint.
* Calls PoolManager.unlock and handles the unlockCallback.
* Starts the AE session via AegisEngine.aeStart and handles aeStartCallback.
* Decodes ExecutionBatch\[] and dispatches action opcodes via \_handleAction.
* Resolves sentinel NFT IDs via TransientNftTracker for composability.
* Wraps Permit2 for approval-free flows (when the user has approved Permit2).

Key interfaces: IAegisRouterV1, AegisRouterActions.

#### AegisHook

The Uniswap v4 hook. One per deployment. Installed on pools to enable AEGIS behavior.

Responsibilities:

* beforeSwap: quotes the dynamic fee via DynamicFeeManager.prepareSwap; adjusts oracle cardinality; stores swap context in EIP-1153 transient storage (HookRuntime).
* afterSwap: finalizes the fee via DynamicFeeManager.finalizeSwap; updates pool-pending fees (\_pendingFees\[poolId]); reinvests into the protocol-owned full-range position when thresholds (MIN\_REINVEST\_LIQUIDITY, REINVEST\_COOLDOWN) are met.
* beforeAddLiquidity / beforeRemoveLiquidity: maintain oracle state; passive on fee accounting.
* Implements IAegisHook; inherits from Uniswap v4's BaseHook, Ownable, Pausable.

External calls (to DynamicFeeManager, OracleManager, AegisEngine) are wrapped in try/catch so that a buggy or paused downstream manager emits ExternalCallFailed rather than bricking the hook.

#### VaultRegistry

An ERC-721 contract that issues vault NFTs. Minted only by AegisEngine. Transfers are blocked by a \_update override when the vault is unlocked in an active session.

### Supporting managers

#### DynamicFeeManager

Maintains per-pool dynamic fee state. Implements IDynamicFeeManager. Hook surface:

* initialize(poolKey) — called once per pool from the hook on afterInitialize.
* prepareSwap(poolKey, params) / finalizeSwap(...) — called from before/after swap.
* getFeeState(poolId) → (base, surge) — view for integrators.

Admin surface: authorizeHook, refreshPolicyCache, setAutoTunePaused.

#### PoolPolicyManager

Read-only registry from the hook stack's perspective. Stores per-pool policy:

* Manual fee overrides (if governance wants to pin a fee).
* Dynamic fee parameters (min/max base fee, surge multiplier, decay period).
* Budgets and cap bounds for the CAP event mechanism.
* Fee splits between LP, protocol, and POL vault.
* Tick-scaling and tick-spacing whitelist.

Admin: onlyOwner setters with parameter range validation via PolicyValidator and PolicyManagerErrors.

#### OracleManager

Per-pool truncated TWAP oracle. Stores observations (ring buffer via TruncatedOracle) and metadata in states. Hook surface:

* recordAfterInitialize(poolKey) — seed the oracle.
* beforeModifyLiquidity, afterSwap — record observations.
* increaseCardinalityNext(poolKey, cardinality) — grow the buffer.

Integrator surface: observe, consult. Rejects unauthorized hooks; rejects secondsAgo = 0 and stale data.

#### LimitOrderManager

Bucket/epoch-based limit order book, integrated as ERC-721 NFT-wrapped order positions. One-tick buckets; approved hooks must call on tick-cross to fill/knock out bucket liquidity. AE treats LO NFTs uniformly with CL NFTs in the √K floor computation.

Router-style entrypoints: modifyLiquidities / modifyLiquiditiesWithoutUnlock, \_place, cancel, withdraw, settlement helpers.

#### VariableInterestRate

Frax-inspired rate oracle with a kinked utilization curve. getNewRate(utilizationPips, elapsed) → (newRate, newFullUtilRate). Construction validates parameter ranges; getNewRate clamps and reverts on invalid inputs.

Per-market configuration: each market can have its own rate oracle via setPoolRateOracle(poolId, addr), falling back to \_defaultRateOracle.

### Two-phase execution

Engine splits router programs into two phases within a single transaction:

#### Phase-0 (PM locked)

Permitted actions: CREATE\_VAULT, UNLOCK\_VAULT, LOCK\_VAULT, ATTACH\_NFT, DETACH\_NFT. These do not touch the PoolManager.

The restriction: Uniswap v4's PositionManager requires PM locked for ERC-721 transfers of CL NFTs. Attach and detach rely on this, so they can't be in Phase-1.

First-touch accrue does NOT fire in Phase-0.

#### Phase-1 (PM unlocked)

Permitted actions: everything else (idle modifications, borrow/repay, peel, micro-liq, modifyLiquidity for sL, limit order operations, settlement).

The first PM-touching action on a given pool in Phase-1 triggers feeSync(); accrue(); exactly once. Cached via EIP-1153 for the remainder of the session.

If a Phase-1-only action appears in a Phase-0 batch, the PM call reverts with ManagerLocked.

### Session lifecycle

user → RouterV1.execute(unlockData, deadline)

&#x20;  → PM.unlock(unlockData)

&#x20;     ↳ RouterV1.unlockCallback(data)           // onlyPoolManager

&#x20;         ↳ AE.aeUnlock(userData)               // opens AE session

&#x20;              ↳ RouterV1.aeUnlockCallback(...) // decode & run actions

&#x20;         ↩ AE epilogue: require unlockedCount == 0

&#x20;     ↩ PM enforces frame-wide delta-zero

Invariants:

* aeUnlock requires the caller to be the router (the pre-registered callback address).
* aeUnlockCallback runs the action batch. Actions can unlock/lock multiple vaults; the session tracks an unlockedCount.
* The AE epilogue requires unlockedCount == 0 at session end. Any still-unlocked vault causes revert.
* The AE epilogue also requires no stray pending NFT attach intent remains.
* The PM epilogue (unlock return) requires the PM's frame-wide NonzeroDeltaCount == 0.

Together these three checks ensure every session ends in a clean state: no mid-mutation vaults, no orphaned NFT intents, no lingering deltas.

### The keeper lane

Most actions require the caller to be the vault owner or an approved operator. Keeper actions are a separate lane:

* PEEL, MICRO\_LIQ, STABILIZE do not require caller authorization on the vault.
* They do require the vault to be locked at call start (they operate on a locked vault by design).
* They still run inside Phase-1 of a PM.unlock frame and an AE session.

This split lets keepers operate without needing per-vault permissions, while preserving session discipline.

### What's constant vs. what's tunable

A short summary of the knob inventory (see Governance and Roles for the full picture):

Compile-time constants (in contracts/libraries/ae/Constants.sol):

* TWAP\_WINDOW\_SECONDS = 30 minutes
* UTILIZATION\_CAP\_PIPS = 950\_000 (95%)
* MAX\_LTV\_PIPS = 980\_000 (98%)
* HARD\_LTV\_BPS = 9\_900 (99%)
* MIN\_LIQUIDITY = 1\_000
* MAX\_NFTS\_PER\_VAULT = 4
* N\_TICKS = 5 (keeper swap band)
* L\_MIN\_TO\_MINT = 1\_000
* PEEL\_BOUNTY\_BPS, MICRO\_LIQ\_FEE\_MAX\_BPS — TODO: enumerate exact values from Constants.sol

Changing these requires a contract redeploy.

Governance-tunable (via Timelock):

* Hook allow-list on AE, OracleManager, LOM, DFM — one-way (approvals are permanent).
* Default and per-pool rate oracle (setDefaultRateOracle, setPoolRateOracle).
* Per-market protocol fee bps (setMarketProtocolFee, bounded 0-10,000 bps).
* DFM policy parameters (fee ranges, surge multipliers, CAP budgets, fee splits).
* DFM per-pool hook fee ppm.

Permissionless:

* Market initialization (AegisEngine.initialize(poolKey)) — anyone can initialize a pool market, provided the hook is authorized and the pool has been initialized on Uniswap v4.
* Vault creation.
* LP deposit/redeem, borrow/repay, attach/detach.
* Keeper stabilize.

### CREATE2 deterministic deployment

AEGIS uses a 4-phase CREATE2 deterministic deployment so that the same code at the same addresses can exist on multiple chains:

* Phase 1 (permissionless): deploy the dependencies bundle (AegisDependencies.sol).
* Phase 2 (trusted deployer): deploy the core contracts (AegisEngine, AegisRouterV1, VaultRegistry, etc.) — requires the AEGIS\_INITIALIZER key.
* Phase 3 (permissionless): deploy the hook and managers.
* Phase 4 (trusted deployer): initialize, authorize hooks, set default rate oracle — requires the AEGIS\_INITIALIZER key again.

The practical effect: if you know the chain ID and the deployer's address, you can compute every AEGIS address in advance. See script/Create2Deploy.s.sol in the aegis-engine repo.

***

For the accounting details — what rL, sL, L, M, borrowIndexWad are exactly, and how they evolve — see [Shares, Debt, and Core Accounting](http://./shares-debt-and-core-accounting.md).

***

## Shares, Debt, and Core Accounting

This page is the accounting cheat sheet. It is compact; anyone implementing against Engine should read it alongside the relevant sections of the Statement of Intended Behavior and the research notes on the L-unit ledger and √K floor.

### The four quantities

Everything in Engine boils down to four per-pool / per-vault quantities:

* L — Uniswap v4 liquidity units on the pool's pooled full-range position. Quantified as uint128 on-chain.
* sL — ERC-6909 lender shares, scoped to tokenId = PoolId. Per-tokenId totalSupply is tracked by ERC6909ClaimsWithSupply.
* rL — borrower real-L principal on a vault. Stored in WAD precision (rLWad).
* M — scale factor between the market's real and nominal L ledgers, per R-0009. Used to convert between real on-pool L and ledgered L.

### Market state

Market.State (in libraries/ae/market/Market.sol) carries:

* bool initialized
* uint128 totalRlBorrowed — sum of rL across all vaults in this market, nominal.
* uint128 borrowIndexWad — compounding index starting at 1e18.
* uint128 equityLWad — total equity in the pooled full-range position, in L-WAD.
* IVariableInterestRate rateOracle — per-pool rate oracle, or zero to fall back to default.
* uint64 fullUtilizationRate — cached full utilization rate, updated on accrue.
* PoolKey key — the pool this market is bound to.
* Residues residues — one-sided fee residues held off-equity until mintable.

### Vault state

Vault.State (in libraries/ae/vault/Vault.sol) carries:

* PoolId poolId — immutable after creation.
* mapping(Currency => int256) idleBalances — signed, allowing temporary intermediate negatives inside a session.
* uint128 rLWad — debt principal in L-WAD.
* NftSet positions — the set of attached CL/LO NFTs, capped at MAX\_NFTS\_PER\_VAULT.

### Share price

sL share price is a simple ratio of equity to supply:

sharePrice = equityLWad / sL\_totalSupply

Two important properties:

* Monotonically non-decreasing in normal operation. Fee reinvestment and interest accrual only add to equityLWad. Lender deposits mint new shares at the current price, so price doesn't dilute.
* Can drop only in one case: if a vault's debt exceeds its collateral floor and keepers cannot fully heal it, the uncovered L is debited from equityLWad, reducing the share price. This is first-loss, borne only by sL holders of the affected pool.

### Borrow / repay accounting

When a borrower calls borrowL(v, dL):

1. Accrue (if first-touch this session per pool) — advances borrowIndexWad by the current rate over the elapsed time, and reinvests mintable fees into equityLWad.
2. Burn full-range L — remove dL × M units of liquidity from the pooled position. Receives (idle0Added, idle1Added) tokens from Uniswap v4, which are added to the vault's idle.
3. Increase debt — market.totalRlBorrowed += dL; vault.rLWad += dL × borrowIndexWad / 1e18.
4. Update market — equityLWad -= dL × M (the burned liquidity leaves equity), net of any fee residues realized during the pre-burn accrue.

The repay path is symmetric:

1. Accrue (first-touch) — same as borrow.
2. Mint full-range L from the provided tokens. Engine reads the actual liquidityMinted back from Uniswap v4.
3. Compute rLEff = floor(liquidityMinted / M). This is conservatively the lower bound on L-units created, ensuring debt is never reduced by more than what was actually minted.
4. Reduce debt — market.totalRlBorrowed -= rLEff; vault.rLWad -= rLEff × borrowIndexWad / 1e18 (clamped at zero).
5. Update market — equityLWad += rLEff × M.

#### The conservative bridge

The reason for M and for floor-rounding: borrowing and repaying happen on a real pool that has continuous-value liquidity math, but the engine ledger tracks debt in discrete nominal L units. The bridge is designed so that:

* Borrow rounds down the real L removed from the pool relative to the nominal dL credited as debt.
* Repay rounds up the real L added to the pool relative to the nominal dL removed as debt.

Together these ensure equity-neutrality as a strict inequality: the real L in the pool is always at least the nominal L recorded in the ledger. The rounding discipline is enforced by LUnitMath and Market.sol.

### Interest accrual

Interest is handled via a single compounding borrowIndexWad:

borrowIndexWad\_new = borrowIndexWad\_old × (1 + rate × dt)

where rate is the per-second rate from the market's rate oracle and dt is seconds since last accrue.

A borrower's real debt is:

realDebt\_L = rLWad × borrowIndexWad / 1e18

Both rLWad and borrowIndexWad persist in storage; the product is computed on-demand when needed (for LTV, repay, or view).

The rate oracle returns two values: newRate (per-second, 1e18 scale) and newFullUtilRate (cached and stored as market.fullUtilizationRate). The Frax-style curve has two regions: below UTIL\_KINK (linear slope upward with time adjustment), above UTIL\_KINK (steep slope).

#### Protocol fee

On every accrue that advances the index, Market.accrue splits the interest earned:

* Protocol share: interestL × marketProtocolFeeBps / 10\_000 minted as new sL to the protocol address (via \_mintProtocolShares).
* Lender share: the remainder flows into equityLWad, raising the sL share price for existing holders.

marketProtocolFeeBps is per-market, bounded 0-10,000, tunable by owner via setMarketProtocolFee. Initial value is chosen at deployment time (TODO: confirm default — working assumption is 500 bps).

### Collateral floor (√K)

Given a vault v on pool p at current √P:

C\_min(v) = sum over {idle balances, attached CL NFTs, attached LO NFTs} of L-burn-value

* Idle balances contribute their geometric-mean-based √K value: √(idle0 × idle1) converted to L-units.
* Attached NFTs contribute the L they would produce if burned at the current √P. For CL NFTs, this uses the NFT's liquidity, tickLower, tickUpper relative to the current tick. For LO NFTs (single-tick buckets), the contribution depends on whether the tick has been crossed.

The SAFE multi-NFT bound (from R-0008) ensures C\_min is conservative for any N ≤ MAX\_NFTS\_PER\_VAULT. The implementation is in CollateralFloorMath.evaluateRaw and TickSetCodec.

The solvency inequality:

rLWad × borrowIndexWad / 1e18 ≤ C\_min × MAX\_LTV\_PIPS / 1\_000\_000

Enforced at lockVault(v) time. A vault in breach after session end causes the transaction to revert.

### Share mint and burn

modifyLiquidity(poolId, sharesDelta, ...):

Mint (sharesDelta > 0):

1. Compute tokens required at current share price.
2. Take tokens from the user via PoolManager's ERC-6909 (pre-funded by the router).
3. Add full-range liquidity to the pooled position (\_poolManager.modifyLiquidity with positive delta).
4. Mint sharesDelta sL to the caller (ERC-6909 \_mint).
5. Update equityLWad by the actual L added.
6. Enforce sharesDelta ≥ MIN\_LIQUIDITY (or revert ERR\_ZERO\_SHARES).

Burn (sharesDelta < 0):

1. Compute L to remove as equityLWad × |sharesDelta| / sL\_totalSupply.
2. Remove liquidity from the pooled position.
3. Burn sL (ERC-6909 \_burn).
4. Credit the user with the resulting tokens via PoolManager.

### Fee accrual mechanics

Fees are earned passively by the pooled position as Uniswap swaps occur. Engine's fee model:

* The hook accumulates pending fees in \_pendingFees\[poolId] (per currency).
* On modifyLiquidity or qualifying reinvest triggers, pending fees above MIN\_REINVEST\_LIQUIDITY are minted back into the pooled position via Market.mintEquityFromFees.
* Only mintable fees count. If the pool is imbalanced such that the fees are single-sided, the one-sided residue stays in market.residues until a future fee collection pairs it up.

This deferral is important: it prevents dust-level fee events from triggering expensive modifyLiquidity calls on every swap.

### The two sets of units

A practical reference for integrators:

| Quantity                          | Type    | Unit                          | Precision |
| --------------------------------- | ------- | ----------------------------- | --------- |
| Token amounts                     | uint256 | wei (18 dec for most ERC-20s) | native    |
| Pool liquidity                    | uint128 | L-units                       | 0 dec     |
| Pool √P                           | uint160 | sqrt price                    | Q64.96    |
| Pool √K (geom mean reserves in L) | uint256 | L-WAD                         | 1e18      |
| WAD numbers                       | uint256 | unitless                      | 1e18      |
| BPS                               | uint256 | unitless                      | 1e4       |
| PIPS                              | uint256 | unitless                      | 1e6       |
| Tick                              | int24   | ticks                         | native    |
| Borrow index                      | uint128 | unitless compound index       | 1e18      |

Rounding discipline: floor on mint, floor on share-price computations; ceil on burns where it protects remaining holders.

### Events

Core Engine events (summary — see spec 0100 §11 for the full list):

* VaultCreated(vaultId, poolId, owner)
* IdleDeposit(vaultId, currency, amount) / IdleWithdraw(vaultId, currency, amount, to)
* BorrowL(vaultId, poolId, dL, idle0Added, idle1Added)
* RepayL(vaultId, poolId, dL, rLEff, xUsed, yUsed)
* FullRangeLiquidityChanged(poolId, liquidityDelta, amount0Used, amount1Used)
* Peel(vaultId, nftKind, tokenId, dL, cMinPre, cMinPost, bountyPaid)
* MicroLiq(vaultId, rL, tickLimit, twapOk, ltvPre, ltvPost, zeroForOne, amountIn)
* UserNftAttached(vaultId, nftKind, tokenId) / UserNftDetached(...)
* HookApproved(hook, allowed)

All numeric event fields carry units in NatSpec.

***

This page summarizes the core accounting. For the authoritative math, read R-0008 (√K static collateral floor), R-0009 (L-unit lender ledger), and R-0010 (peel + micro-liquidation) in the aegis-engine/docs/research/ directory of the repo.
