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 │

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

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

│ AegisEngine (Core Singleton) │

│ • Vault lifecycle (ERC-721) │

│ • sL shares (ERC-6909) │

│ • L-unit ledger │

│ • √K solvency checks │

│ • Session management │

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

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

│ 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)

→ PM.unlock(unlockData)

↳ RouterV1.unlockCallback(data) // onlyPoolManager

↳ AE.aeUnlock(userData) // opens AE session

↳ RouterV1.aeUnlockCallback(...) // decode & run actions

↩ AE epilogue: require unlockedCount == 0

↩ 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 Accountingarrow-up-right.


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.

Last updated