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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.aegis.markets/part-4-system-structure/system-architecture-and-contract-roles.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
