> For the complete documentation index, see [llms.txt](https://docs.jaawle.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.jaawle.xyz/security/design.md).

# Design

The system is a parent vault over child pools (the GLV-over-GM shape). One `MarketPool` per market holds that market's shares and USDC and is the sole counterparty for its perp. One `IndexVault` holds child LP tokens at target weights. Synthetic markets (crypto, meme, commodity) use the same pool with no token: cash only, both sides capped on USDC. See [Pools and NAV](/protocol/pools-and-nav.md) and [Overview](/protocol/overview.md).

The design was written against a review of a comparable parent/child vault and its audit. Every rule below exists to close a gap that review found, or one the issuer's settlement latency creates.

## Pricing is fail-closed

A price feed that returns a fallback instead of reverting mints shares against a wrong NAV. `PriceFeed.getPrice` reverts instead:

| Condition                                                  | Result                                       |
| ---------------------------------------------------------- | -------------------------------------------- |
| `publishTime` older than `maxAge`                          | revert (staleness)                           |
| `conf × 10 000 > mid × maxConfBps`                         | revert `ConfidenceTooWide`                   |
| jump above `maxDeviationBps` since the last accepted price | revert `DeviationTooLarge` (circuit breaker) |

Two prices, never a mid for both legs: deposits value shares at **bid** (`mid − conf`), redemptions at **ask** (`mid + conf`). The LP always gets the side that favours the pool.

The session is part of the price. The settler pushes `Regular`, `Extended` or `Closed`; a session older than `sessionMaxAge` (2 hours) reads as `Closed` on its own, so a dead keeper cannot leave the exchange "open".

### Closed market: a closing mark, not a freeze

Equity feeds go quiet after the close and their confidence widens. A feed that only reverts on staleness would take the pool with it from about 16:10 ET to the next open and all weekend: NAV, USDC deposits and redemptions, the index, liquidation checks. Instead, while the session is `Closed` and the feed is stale, an asset that follows market hours is served its last accepted price as a **closing mark**: `bid = ask = mid`, `session = Closed`, valid for `maxMarkAge` (default 4 days). A live print while Closed (a halt, a late tick) is still preferred over the mark.

The rule for callers is: nothing that moves value at a stale price.

| Caller                                        | At the mark                                                                                                                                                    |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MarketPool` USDC deposit / redemption        | queued; executed at the first live print (`SessionClosed` skips it in the batch), so whoever knows the next gap cannot deposit or redeem against the old price |
| `MarketPool` stock legs, issuer orders        | regular session only                                                                                                                                           |
| `IndexVault` routing into and out of children | waits (goes through the child's open check)                                                                                                                    |
| `PositionManager` open                        | regular session only                                                                                                                                           |
| `PositionManager` close, liquidate, trigger   | refused while `Closed` (`SessionClosed`); the position is still *detected* as liquidatable at the mark                                                         |
| NAV, funding, borrow accrual                  | measured at the mark                                                                                                                                           |

Off hours (`Extended`) are different from `Closed`: closes and liquidations run at a doubled fee (`offHoursFeeMult`) and a higher maintenance requirement (`offHoursMaintenanceMult`).

The keeper adds one more guard on its side: while the exchange is Closed it does not push stock prices on-chain at all, even though the tokenized feeds print around the clock, and it keeps any print whose confidence is wider than `maxConfBps` off-chain rather than let the feed refuse every read of it. See [Price feeds](/assets-and-prices/price-feeds.md).

### Dividends

The perp index is spot. On ex-date the gap is compensated per position by `PositionManager.applyDividend`: longs are credited, shorts debited, through a cumulative per-token index. The pool funds the long credit and receives the short debit, and it can always afford it because reserved long tokens never exceed its inventory.

The dividend cash pushed to the pool is LP yield. `sweepDividends` keeps what the pass-through needs (`(longTokens − shortTokens) × perShare`) and books the rest into NAV. `dividendFeeBps` exists as a config knob and is deployed at 0 everywhere, so nothing leaves for the treasury. See [Dividends](/protocol/dividends.md).

## Pending issuer orders are counted in NAV

The issuer settles over hours. USDC that has left for a buy is not lost, and shares committed to a sell are not free, so both must stay in `totalAssets()`:

* `placeIssuerOrder` escrows the USDC (or the shares) into a `PendingOrder`; `pendingUsdc` / `pendingStock` are part of NAV.
* `settleIssuerOrder` books the fill, pulls back the unfilled remainder (partial fills off hours are normal) and reverts if the average fill price is further than `maxFillDeviationBps` from the oracle mid.
* `refundStaleOrder` returns the escrow after `maxPendingAge`, so a lost order can never freeze accounting.

Because escrow is in NAV, NAV is computable from chain state alone; no attestation of issuer-side holdings is needed. See [Issuer orders](/protocol/issuer-orders.md).

## Request, then keeper execute, priced at execution

Every deposit and redemption on a pool or the index is a request carrying `minOut` and `expiresAt` (request TTL 3 days). The keeper executes at execution-time NAV; a request that misses `minOut` reverts with `SlippageExceeded` and stays queued; after expiry the user calls `cancelRequest` and reclaims the escrow. No price is ever locked in at request time.

## Share-price manipulation

* **Virtual offset.** `convertToShares` and `convertToAssets` add `VIRTUAL_SHARES = 1e18` and `VIRTUAL_ASSETS = 1e18` (one dollar) to supply and NAV, on both the pool and the index. Inflating the share price by donating to an empty vault costs more than it can return.
* **Rounding.** Shares minted round down; shares burned on redemption round up (the LP bears the dust, never the pool).
* **Minimum deposit.** `minDepositUsd` on both pool and index; dust requests are refused at request time.
* **Internal accounting.** The pool tracks `usdcBalance`, `usdPlusBalance`, `stockBalance`, `escrowUsdc`, `escrowStock`, `pendingUsdc` and `pendingStock` itself and never values itself from `balanceOf`. A token sent to the pool outside a request does not move NAV. The only reads of `balanceOf` are explicit, role-gated reconciliations: `sweepDividends` (books the cash that arrived, refused while orders are in flight) and `syncInventory` (realigns after a split, refused while orders are in flight).
* `skim` can rescue a stray token but never USDC, USD+, the shares or the LP token.

The deployed defence is the virtual offset; every deployment also seeds each pool with liquidity before users arrive.

## The keeper is bounded on-chain

The keeper supplies `minOut` and timing; the contracts enforce the envelope:

| Bound                                                             | Where                                       |
| ----------------------------------------------------------------- | ------------------------------------------- |
| per-order USD cap `maxOrderUsd`                                   | `placeIssuerOrder`                          |
| deploy ratio (stock value / NAV) `maxDeployRatioBps`              | `placeIssuerOrder`                          |
| USDC reserve floor `reserveBps` (`idleUsdc`)                      | issuer buys, USDC redemptions, parent pulls |
| free inventory only (`stockBalance − reservedStock`)              | issuer sells, stock redemptions             |
| long OI at most inventory; short OI at most `shortCapBps` of USDC | `PositionManager` through the pool's hooks  |
| `minRebalanceInterval` and `maxShiftBps` of NAV                   | `IndexVault.shift`                          |
| regular session only                                              | issuer orders, stock legs, opens            |

The keeper can never change config, weights or upgrade anything. Batch loops (`executeRequests`) `continue` on a failed item and never revert the batch; the keeper always sends them with an explicit gas limit, because gas estimation can starve the inner call and make an empty batch look successful.

## Parent and child accounting

* The parent values a child by **its own LP balance × the child's share price**, never by the child's TVL.
* The parent pulls only a child's **idle** USDC (after the reserve floor and queued redemptions), proportionally.
* The parent checks `child.isPaused()` before every interaction and skips a paused child when routing, so one paused child cannot stop deposits or redemptions routed to the others.

## Access, reentrancy, upgrades

One `AccessRegistry` holds roles, the KYC allowlist and per-contract pause flags. See [Governance and roles](/protocol/governance-and-roles.md).

| Role               | Holder                                         | May                                                                                                 |
| ------------------ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `ADMIN`            | timelock (plus a dev EOA on local chains only) | config, weights, wiring, upgrades, KYC allowlist                                                    |
| `KEEPER`           | keeper key                                     | execute requests, place issuer orders, shift the index, liquidate, execute triggers, refresh prices |
| `SETTLER`          | issuer adapter wallet                          | settle and refund issuer orders, push the session                                                   |
| `POSITION_MANAGER` | the PositionManager                            | counterparty hooks on pools, draw on insurance                                                      |
| `GUARDIAN`         | guardian key                                   | pause only, never unpause                                                                           |
| `FACTORY`          | the PoolFactory                                | register new markets                                                                                |
| `ROUTER`           | TradeRouter and TriggerBook                    | act for an account through the `…For` entry points                                                  |

* `ReentrancyGuardTransient` on every external entry point and settlement callback; `msg.sender` is always checked against the registry, never trusted from ordering alone.
* The `…For(account)` entry points are `onlyRole(ROUTER)`, run the KYC check on `account`, pull funds from the router and pay out to `account`. The plain `msg.sender` paths are untouched, so a user can always act without the relayer. See [One-click trading](/product/one-click-trading.md).
* The TriggerBook executes a take-profit or stop-loss as a decrease on the PositionManager, so the payout goes to the account; the keeper has priority, then anyone may execute `priorityWindow` seconds after a public `flag`. A trigger remembers the position's `openedAt`, so one left on a closed position never fires on a new one.
* KYC is Reg S, non-US only. The allowlist is written by `ADMIN` (locally the keeper key is granted `ADMIN` for this; in production the write goes through the timelock). Evidence comes from an on-chain attestation and a sanctions oracle, an operator review, or an invite code during the closed beta. See [Eligibility and identity](/legal/eligibility-and-identity.md).

## Insurance vault

The vault pays only against **realised bad debt**, inside the liquidation transaction, so the pool is made whole before any mint or redeem can be priced against the loss. There is no drawdown proxy, no snapshot and no trigger that defaults to "pay".

* `cover` is callable only by the `POSITION_MANAGER` role.
* Caps are explicit: `maxCoverPerTx` and `maxCoverPerDay`, and both default to zero, so an unconfigured vault is **off**. Staging runs 50 000 / 200 000 USDC.
* One reserve asset (USDC). No LP shares: capital is the insurance cut of fees (`insuranceFeeBps`, 10 %), liquidation remainders and treasury top-ups; only `ADMIN` (the timelock) withdraws.
* Whatever insurance cannot cover is added to `PositionManager.badDebtUsd` and emitted, so a shortfall is visible rather than silently absorbed.

See [Liquidation and insurance](/risk/liquidation-and-insurance.md).

## Upgrade model

```mermaid
flowchart LR
  TL[Timelock<br/>ADMIN] -->|owner| B[UpgradeableBeacon]
  B --> P1[BeaconProxy · MarketPool AAPL]
  B --> P2[BeaconProxy · MarketPool NVDA]
  B --> Pn[BeaconProxy · …]
  TL -->|_authorizeUpgrade| U1[ERC1967Proxy · PositionManager]
  TL -->|_authorizeUpgrade| U2[ERC1967Proxy · IndexVault]
  TL -->|_authorizeUpgrade| U3[ERC1967Proxy · PriceFeed · InsuranceVault · PoolFactory · TradeRouter · TriggerBook]
```

* Child pools: `BeaconProxy` → `UpgradeableBeacon`, owner = timelock. One upgrade moves every pool.
* Singletons: UUPS behind `ERC1967Proxy`; `_authorizeUpgrade` is gated on `ADMIN`, which is the timelock.
* ERC-7201 namespaced storage everywhere; new fields are appended; implementations call `_disableInitializers`.
* Bootstrap: `BaseDeploy` deploys one contract per transaction (CREATE2, so a fresh chain gets the same addresses), grants the roles, hands `ADMIN` to the timelock and renounces the broadcaster's `ADMIN` unless it is the declared `devAdmin` of a local fork. Tests use `Deployer`, which does the same in one constructor.
* No DataStore: proxies already give upgradeability, and a typed storage layout is safer than an untyped controller surface.
* Local chains run the timelock with `minDelay = 0`; `UpgradeLocal` upgrades a running deployment in place. See Local development.

## Platform revenue

Platform revenue is the **protocol fee**: `protocolFeeBps` (10 %) of taker fees and of the borrow + funding leg, paid to the `PositionManager` treasury at settlement (`TreasuryUnset` reverts if no treasury is configured), plus **10 bps** on a user's dShare purchases through the Stocks page. The insurance cut (also 10 %) goes to the insurance vault. Dividends are not platform revenue. See [Fees](/product/fees.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://docs.jaawle.xyz/security/design.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
