---
title: "Anchor path pricing"
description: "Multi-anchor tree, MAX_DEPTH = 4: every asset anchors to one correlated parent, not necessarily the base. A swap is the unique tree path between its endpoints, priced and settled once inside a single swap call."
audience: tech
type: explanation
status: live
lang: en
updated: "2026-09-02"
publish: true
---
# Anchor path pricing

A swap inside one pool is not a pair trade against a numeraire: it is a walk along the anchor tree
between the two endpoint assets. This page states that topology, how each leg of the walk is priced,
what settles and what does not, and the conditions under which a closed cycle through the tree cannot
return more than it consumed. §1 gives the shape; §3 to §5 give the pricing law and the fee
composition that keep it safe at depth.

A pool based on **BTC or ETH** still quotes tight stable-to-stable and LST-to-underlying swaps
because each asset anchors to its **correlated parent** rather than to the pool base:
`USDT -> USDC`, `stETH -> ETH`, `sUSDe -> USDe`. The traded pair is one edge of the tree, priced off
one feed with one sigma, never routed through an uncorrelated numeraire.

## 1. Topology

> Canonical. The depth bound, the leg count and "topology is configuration" are stated here and
> pointed at from [Spread & Fees §10.1](/docs/1-1-4-spread-fees#101-routing),
> [Parametrization §12.1](/docs/1-1-7-parametrization#121-topology-configuration),
> [Invariants I-15](/docs/1-1-8-invariants#i-15-tree-structure-bounded-tree-maxdepth--4) and
> [Pool §7](/docs/1-2-1-pool#7-swap-paths-anchor-tree).

```mermaid
graph TD
    WBTC[WBTC base]
    WBTC --> WETH
    WBTC --> USDC
    WETH --> stETH
    WETH --> weETH
    USDC --> USDT
    USDC --> DAI
    USDT --> USDe
    USDe --> sUSDe
```

- **Base**: the root and the accounting numeraire. `anchor = address(0)`.
- **Every other asset**: exactly one parent, any asset in the tree, including a non-base one.
- **Depth**: `MAX_DEPTH = 4`. A path is two walks that meet at the LCA, so the bound is
  `MAX_PATH_LENGTH = 2 * MAX_DEPTH + 1 = 9` nodes and `2 * MAX_DEPTH = 8` legs, not 5 and 4. The
  worst case is two depth-4 leaves whose LCA is the root: 4 legs up, 4 legs down. Depth bounds one
  walk; a path is two of them.
- **Path**: unique. In a tree exactly one simple path joins two nodes: up from the source to the
  lowest common ancestor (LCA), then down to the destination. Nothing to search: no route-selection
  surface, and no route path-dependence.
- **Endpoints**: the two ends of the path. Every other node on the path is **interior**.

| Swap | Path | Legs |
|------|------|------|
| child to parent | `stETH -> WETH` | 1 |
| siblings under a common parent | `USDT -> USDC -> DAI` | 2 |
| deep leaf to the root | `sUSDe -> USDe -> USDT -> USDC -> WBTC` | 4 |
| across the root, both sides deep | `stETH -> WETH -> WBTC -> USDC -> USDT -> USDe -> sUSDe` | 6 |
| worst case at `MAX_DEPTH = 4` | two depth-4 leaves under different root children | 8 |

The tree drawn above produces a 6-leg path, and it is not the worst it admits: a second depth-4
branch under `WETH` gives 8. Every "sum over legs" budget in this document is sized against 8, not
against the shape of the example.

`USDT -> DAI` in a WBTC-based pool touches one interior node (USDC) and prices off two stable feeds.
It never consults the BTC mark.

### 1.1. What the anchor column configures

The contracts enforce the topology generally: `validateAnchor` accepts any parent up to
`MAX_DEPTH = 4`, with cycle detection, detachment detection and LCA routing. What a pool runs is configuration: the `anchor` column of its risk-param file is the sole expression of its topology. An empty cell means base-anchored; filling one cell deepens the tree, no redeploy needed.

A flat roster (every leg base-anchored) makes each leg an endpoint leg (`_executeLeg` marks a leg interior only when the edge's child is not one of the swap's two endpoints), so every route is one or two legs. The interior fence of [§3.2](#32-the-interior-fence) is live code either way: the moment one `anchor` cell is filled it prices every transit. Its second-order margin has market tape only once deep routes run; until then it is pinned by tests.

The schema, the deploy scripts and every fence budget on this page stay general to
`AnchorTreeLib.MAX_DEPTH = 4`; nothing special-cases a shallower tree. A deep edge needs the cross mark to exist: NX Rates must serve the pair as a first-class series with its own σ and its own push trigger (a pair registered in `cexs.cross_pairs` but excluded from materialization has no live mark and no sigma). Once NXR serves the cross, activation is one column change (write the parent into the child's `anchor`) plus `addAnchorFeeds()` -> `requestAnchorTree()` -> (the `CRITICAL` tier delay) -> `executeAnchorTree()`. Every deep leg must carry `quoteUnit = 0` (`QUOTE_UNIT_ANCHOR`) and its own
reference feed: the shared USDC/USD reference speaks USD and cannot bound an anchor-denominated mark.

## 2. Settlement: endpoints only

A swap mutates exactly two reserves:

```solidity
aIn.reserves  += amtIn;
aOut.reserves -= amountOut + protoFee;
```

`PoolIOLib.sol`. This holds at any depth. Interior assets are traversed
for pricing and never credited or debited, so `dR_interior = 0` and no path can drain an interior
node. Two consequences:

- The **coverage toll** stays terminal-only, charged once on the out endpoint
  (`Pricing._covToll`, called once from `Pricing._settleQuote`). `dQ == 0` on interiors by
  construction, so a per-leg toll would charge for a displacement that did not happen.
- **Impact is charged on endpoint legs only.** See section 3.

## 3. Leg pricing

| Leg | Mark | Skew | Spline impact | Reserves |
|-----|------|------|---------------|----------|
| endpoint | parent-per-child mark | yes | yes | yes |
| interior | parent-per-child mark | yes | **no** | no |

**Skew applies on interior legs.** Skew is pricing, not a charge for inventory change. If the interior
asset is under-covered its worth to the pool is depressed, and pricing the leg at the undepressed
mark hands the trader that asset's depeg risk for free.

**Interior skew cancels on a round trip only against unchanged interior coverage.** Skew is a level,
so the cancellation is exact **within** one transaction and does not extend across transactions:
skew is a function of the interior node's coverage, so the out-leg and the return leg read the same
level only if nothing moved that coverage in between. An interposed trade that shifts an interior
node's coverage between the two crossings breaks the cancellation, and the residue is extractable.
Measured on the reference model: **+2.96 bp** under the
`max` aggregation the code shipped at the time, **+1.01 bp** under sum aggregation.

### 3.1. One pricing law per edge

Interior and terminal legs call the **same expression**, `Pricing._legMid`
(`Pricing.sol`, called from `_interiorMidAndFence`). An edge is
terminal on the routes whose endpoint is its child and interior on every other route, and a mid-tree
anchor is a legal swap endpoint, so both readings are live at once. A correction applied on one side
only (a re-centering, a clamp) puts **two prices on one edge at one state**, and a closed walk that
crosses it once under each law extracts the difference atomically: no manipulation, no capital at
risk, no state change.

**The stored curve is centered at the write.** `NUQuartic.set` shifts the submitted polygon so
`y(0) + y(BPS) == 0` before building the segments (`NUQuartic._centre`, `NUQuartic.sol`). It is
shape-preserving (the fitted density $y'$ is untouched), idempotent, and exact but for the 1 Q-unit
an odd sum cannot split. Consequence: the stored offset never exceeds $w/2$ for **every** stored
shape, so no read path corrects for the fit and no admission bound on $\beta = y(0)/\Sigma$ is
needed: the shift pins it at $-1/2$ for every curve the pool will ever hold.

### 3.2. The interior fence

An interior leg contributes `max(minFee_i, fence_i)` to the path floor
(`Pricing._priceInteriorLeg`), where

$$F_i = \left\lceil\frac{w_i \cdot \text{PBPS}}{\text{PBPS} - \bar w/2}\right\rceil,
\qquad w_i = \left\lceil\frac{\Sigma_i \cdot \kappa_i}{Q\cdot\rho_i}\right\rceil,
\qquad \bar w = 10{,}000\ \text{PBPS}$$

$F_i$ is the fence, $w_i$ the leg's mid swing, $\Sigma_i$ its curve's span, $\kappa_i$ its live
dispersion, $\rho_i$ its `dispRefPbps`, and $\bar w$ = `INTERIOR_SWING_CAP_PBPS`, a file-private
constant in `Pricing.sol`.

**The swing is peak-to-peak, and the two readings differ by a factor of 2.** `cap` = 10,000 PBPS is a
**1%-of-mark total range**, i.e. a $\pm 0.5\%$ one-sided displacement either side of the mid. The
code uses both readings and names the half explicitly rather than dividing inline:
`INTERIOR_HALF_SWING_PBPS = INTERIOR_SWING_CAP_PBPS / 2 = 5000`, and the fence's constant denominator
is `FENCE_LOW_MUL_PBPS = PBPS - INTERIOR_HALF_SWING_PBPS`. `_interiorMidAndFence` compares a
**peak-to-peak** swing against `cap`; `_fenceOfSwingPbps` divides by the **half**-derived multiplier. Any
statement that reads `cap` as a one-sided $\pm 1\%$ band, or the denominator as `PBPS - cap`, is off
by 2 in one direction or the other.

The extraction a cross-transaction coverage manipulation realizes is a price **ratio**, not an
offset, so the fence is a ratio too, taken over a **constant** low multiplier (`Pricing._fenceOfSwingPbps`)
rather than the leg's own realized one. It is computed **per quote**: it reads the leg's live
dispersion and its curve's span, never a stored fee. Its per-leg and composed ceilings, and why the
`uint16` spread field cannot saturate them away, are derived at
[Spread & Fees §9](/docs/1-1-4-spread-fees#9-why-the-path-fee-floor-sums-over-legs).

**The fence is the leg's fee floor, not a side channel.** It folds straight into `r.minFee` as
`max(minFee_i, fence_i)` at its one consumer and contributes to `acc.minFeePath` exactly like any
other leg floor. There is no separate accumulator carrying it, and nothing bounds it from above:
the path spread is a sum of floors and surcharges with no cap
([Spread & Fees §3.1](/docs/1-1-4-spread-fees#31-definition)).

**Swing above the cap reverts; it is never clamped** (`Pricing._interiorMidAndFence`). A clamp is a
law fork by construction, because it bites on the interior leg only, which is the defect in 3.1.
Failing closed cannot fork it.

**The dispersion floor is bound at the write.** `Pricing.dispersionCap` sizes the widest band a
preset's fence can bound, $\bar w\,\rho\,Q/\Sigma$, and `PoolConfig.sanitizeDispersion`
checks `minDispersionPbps` against it (`PoolConfig.sol`) at both writers (`initAsset` and
`setProfile`), so a live asset never starts past the cap. That check reverts, never clamps:
silently narrowing `minDispersionPbps` would move the asset's own quiet-tape quote, so a floor above
the cap is a config error. Above the floor, σ drives κ one-for-one (at $\nu=10000$) up to the
protocol-wide `MAX_DISPERSION_PBPS` = 900000 - no per-asset ceiling field exists; κ past the shape
cap fails closed on the interior swing instead. Shipped preset caps: **5000** PBPS for presets 1, 2 and 5, **2500**
for preset 3, **1000** for preset 4. No shipped `minDispersionPbps` exceeds its cap (widest is KRW1 at
3740 under 5000).

Cost: the fence is the leg's live swing plus 0.5%, e.g. 324 PBPS on USDT (minDispersion
161, ratio 2) and 4089 on WETH. On a quiet tape the endpoint `minFee` is usually the binding floor,
not this.

### 3.3. Rejected alternatives

Recorded so they are not retried:

| Alternative | Measured | Why it fails |
|---|---|---|
| Clamp interior legs against interior reserves | -9900 bp | Caps a routing quantity against a balance the swap never spends. Destroys the tree: any cross larger than the thinnest interior node quotes short. |
| Extend the coverage wall `kappa` to interiors | no-op | The toll is terminal-only by construction: `dQ = 0` on an interior because `dR_interior = 0`, so any `kappa` multiplies zero. |
| Freeze the edge rate for the duration of a path | no-op | The manipulation is cross-transaction. Freezing within one quote fixes nothing that was not already consistent within one quote. |

### 3.4. Why the fence is a protocol constant

The attack class is **route-dependent price inconsistency on one edge**: an attacker crosses the
same edge twice, once as an interior leg and once as a terminal one, and pockets the difference.
It is closed by law identity (3.1), not by making the fence larger.

Four fence shapes fail against it, each failure a property of the shape rather than its size:

- **A multiple of dispersion.** Assumes a swing of some fixed multiple of the dispersion, which no
  spline has. The real swing is $\Sigma\kappa/(Q\rho)$, and nothing bounds
  that ratio at write time; the shipped book spans ratios 2, 4 and 10, so one constant under-charges
  the wide shapes by up to 5x.
- **The curve's own span, charged linearly.** A linear fee against a **ratio** extraction defends to
  first order only: the round trip nets $-x^2(3/4 + \beta)$, so any shape below $\beta = -3/4$ is
  extractable.
- **Ratio-correct, sized per crossing.** Fencing each crossing at *its own* live dispersion lets the
  cycle net $(1/2+\beta)(x_2 - x_1)$: the attacker picks which crossing is the volatile one. Linear
  in $\beta$, so bounding $\beta$ does not close it.
- **Sized at each crossing's live dispersion.** With adaptive dispersion (the 2026-08-21 adaptive-dispersion change) σ itself
  moves the band between crossings, and the profile queue is timelocked but **public**, so the
  two crossings are schedulable either side of the execute and the config change itself becomes the
  channel. The product also overflows the `uint16` spread at wide bands.

The design that holds is a protocol-constant swing cap with the interior mid re-centered at the
write, one `_legMid` on every route, a revert above the swing cap, and the band bound at the write.
Applying a correction to the interior leg but not the terminal one is what gives one edge two
prices, so both corrections are applied on both. Coverage lives in the anchor-tree regression suite (the
cross-law cycle asserted unprofitable at every band, the same edge priced on both routes at once, a
mid-tree anchor settling as a real endpoint) and in the pricing-fence tests.

**Impact must not be charged on interior legs.** `dR_interior = 0`, so charging spline impact invents
a cost for a displacement that did not occur, monetizes path-dependence, and replaces an exact
round-trip identity with a size coincidence. The revenue argument for charging it is answered by fee
composition (section 5) instead.

## 4. Cycle safety

A path is a unique tree path, so any closed trading cycle is a closed walk in a tree and therefore
crosses every edge equally often upward and downward. Per-edge safety then gives cycle safety at any
length and any depth:

$$\Pi_{\text{cycle}} < 1 \quad \Longleftarrow \quad C_0 \wedge C_1 \wedge C_2$$

**C0. Marks in parent units.** Every leg mark is attested as parent-per-child, never base-per-asset.
See section 6.

**C1. One canonical integer per edge.** Direction is expressed as multiply versus divide, never as a
second construction:

```text
M_e = _legMid(mark_parent_per_child, dispersion_child, curve_child, skew_child)

upward   (child -> parent):  out = in * M_e / WAD
downward (parent -> child):  out = in * WAD / M_e
```

Never materialize `1/M`. Never re-apply skew after inverting a rate: that is the reciprocity defect
described in section 9.

**C2. The spline is monotone nondecreasing.** True by construction for the clamped quartic I-spline
(`NUQuartic`), so it costs nothing to hold.

**Integer discipline.** Double flooring gives `dn(up(x)) <= x` and `up(dn(x)) <= x` for every `x` and
every `M`, with no rounding hypothesis. This needs no fuzz evidence: floor is monotone and the two
maps compose to the identity, so the sign result is unconditional. It bounds the sign, not the
magnitude; the residual in wei is a function of `M`, of the decimal pair and of the sampling domain,
so no single number characterizes it. Any *bias* in `M` cancels, because both directions consume the
same integer. Only a *direction-dependent construction* of `M` can leak. Enforce it in code, not in
prose:

```solidity
require(isUpward ? out * WAD <= in * M : out * M <= in * WAD);
```

## 5. Fee composition over the path

Each leg ships `minFee_i >= 2 * theta_i`, so summing both sides gives the path its fence for free.

Every risk aggregate composes over the leg multiset; none of them reduces by `max`.

| Term | Aggregation | Site |
|------|-------------|------|
| `minFeePath` | **sum** over all legs, each leg's floor already raised to `max(minFee_i, fence_i)` on an interior leg | `_walkLegs`, floor at `_priceInteriorLeg` |
| `sigmaSqPath` | **quadrature**, $\sqrt{\sum_i \sigma_i^2}$, one `sqrt` at the end | `_walkLegs`, rooted at `_pathSpread` |
| `riskPath` (confidence + staleness) | **sum** over all legs of $z\sigma_i\sqrt{\tau_i}/\text{BPS} + u_i\cdot(\text{PBPS}/\text{BPS})$ | `_walkLegs` |
| upper bound on the composed spread | **none, deliberately**: see below | `_pathSpread` doc comment |

`max` is structurally under-fenced. An adversary picks the joint worst realization, so the path error
is the **sum** of the per-leg thetas while `max` funds one leg. Quadrature for sigma matches what NX
Rates already does when it composes a bridged pair. The staleness surcharge sums per leg rather than
evaluating one coupled term at `(max_i tau_i, sigma_path)`: one keeper feeds several edges, so an
outage staleses them together, and the coupled form charged two equally stale legs
$\sqrt{2}/2 = 70.7\%$ of what the legs each owe.

Every row says "over all legs", interior ones included. That replaces charging impact on an interior
leg: the interior leg pays through the fence, not through a fabricated displacement.

**Nothing bounds the composed spread from above, by design.** Trader protection is `minAmountOut`:
exact, caller-set, per trade, bounding the quantity the trader cares about. A protocol-side cap on
the quote would instead cap the pool's own defense on exactly the tape where the premium must be
paid, since a stale or high-CI leg is when a wide quote is the correct one.
Full argument: [Spread & Fees §3.1](/docs/1-1-4-spread-fees#31-definition).

**Overflow.** The leg sums cannot wrap, and the single narrowing into `SwapQuote.spreadPbps`
saturates rather than wrapping, so what saturates away is the risk premium and never the security
floor. Arithmetic and the pinning test: [Spread & Fees §9](/docs/1-1-4-spread-fees#9-why-the-path-fee-floor-sums-over-legs).

## 6. Marks and denomination

> The feed for asset X must be attested in units of `assets[X].anchor`. The pool performs no
> re-denomination: `mark = Oracle.mark(feed)`.

At depth >= 2 the base-denominated rule is dimensionally wrong, not merely imprecise: composing
`stETH -> ETH -> USDC` from base-quoted marks yields `stETH * ETH / USDC^2`.

- `OracleConfig.quoteUnit` (`uint8`) declares how a leg's feed is denominated.
  `QUOTE_UNIT_ANCHOR = 0` is the norm; `QUOTE_UNIT_UOA = 1` is the unit-of-account bridge, and a
  bridged leg's `minFee` must cover `2 * (theta_child + theta_parent)`
  (`PoolConstantsLib.sol`).
  Read `QUOTE_UNIT_UOA` as the **instruction "divide by the base mark"**, not as a unit: it is legal
  only while the asset anchors directly to the base (`PoolConfig.sol` rejects it otherwise),
  because dividing by the base price says nothing about a deeper parent.
- An anchor-quoted mark is consumed exactly as attested, with no re-denomination helper in the path.
  The UOA division is three lines inside `Pricing._legMarkAndFees`, reusing the same gated,
  depeg-banded base read the path already performs.
- **The depeg gate is feed-relative, never absolute.** It is the pair `refFeedId` / `refBandBps` in
  `OracleConfig`, run on every interior node by `PoolIOLib.priceBandGuardPath` and on both endpoints by
  `PoolIOLib.priceBandGuard`, plus `BASE_DEPEG_HALT_BPS = 500` on the base. An absolute price band would
  have to be denominated in something, and under multi-anchor there is no one unit to denominate it
  in; a same-unit comparison against a second attestation has no such problem (section 7.1). The
  absolute-band policy call belongs to the guardian halt.
- Multi-leg marks telescope algebraically. Residual is at most 1 ulp per leg.

## 7. NX Rates data contract, per anchored pair

An anchored pair is a first-class series, not a quotient computed at read time. For each edge
`(child, parent)`:

- Its **own catalog index**, its own on-chain `feedId = keccak256(child, parent)`, its own persisted
  `.s10` history, its own Parkinson sigma, and its own keeper push trigger with `theta_bps` derived
  from the **cross's** realized volatility.
- **Pinned synth preferred**: `SynthPath::new("DAI/USDC", [DAI/USD +1, USDC/USD -1])`. A pinned
  quotient of one consistent USD vector telescopes exactly and still carries its own sigma and its own
  push trigger.
- **Never `derive_legs`** for an anchored pair. It returns the first pivot that merely resolves, which
  may already have latched a dead book.
- **Avoid a native venue book** for an anchored pair unless the basis is fenced. An independent
  stETH/ETH book can disagree with `stETH/USD divided by ETH/USD`, and no fence measures that
  composition error. The intra-pool cycle stays safe either way; the pool would quote a stale
  composite.
- If the ratio is not its own push trigger the fee is **under-funded**: both legs can sit inside their
  own theta bands while the ratio drifts to the sum of them, and no push fires.
- **Depeg reference**: see section 7.1. The reference feed must be attested in the **same unit as the
  primary**, so an anchored pair references the anchored pair, never the absolute leg.

### 7.1. What `refFeedId` is, and what it is not

`OracleConfig.refFeedId` / `refPrimary` is an **agreement check between two independent attestations
of the same quantity**. It is not a peg test.

The band compares the primary mark against the reference mark in the same unit:

```solidity
uint256 dev = pRaw > refP ? pRaw - refP : refP - pRaw;
if (dev * SC.BPS > refP * uint256(oc.refBandBps)) revert Err.PriceOutsideRefBand(pRaw, refP);
```

`PoolIOLib.sol`. By convention `refFeedId` is `keccak256(token, USDC)` on a
**distinct oracle instance**: `PoolConfig.validateOracleConfig` rejects `refPrimary == primary`
(`PoolConfig.sol`) and proves both reachable. So the reference is a second,
independent signer set attesting the **same pair**, and the band measures how far the two disagree.
It is a manipulation cross-check, and it fails closed on a stale, dead or over-uncertain reference.

**Pointing it at the absolute leg would brick the pool.** For a primary of `stETH/ETH` at roughly
1.15, a reference of `stETH/USD` at roughly 4000 gives `dev / refP` of about **99.97%**, past any
band a `uint16` can hold. The first swap reverts and every subsequent one does too. This is not a
tuning problem: the two feeds are not the same quantity, so their difference is not a deviation.

**A parent-depeg breaker does not exist, in any form.** Making `stETH/ETH` say something about ETH
needs a **new field**, either `absFeedId` plus `absBandBps` carrying the child-versus-USD attestation
alongside the anchored primary, or a split of `refFeedId` into an agreement band and a separate
absolute-depeg band. Neither is built. Until one is, an anchored child inherits its parent's depeg
risk with no automatic breaker, and the only lever is the **guardian halt** (`haltAsset(pool, token, src)` with the guardian bit): the per-asset
`Asset` carries no absolute price band, and the parity halt at `BASE_DEPEG_HALT_BPS = 500` tests the
**base mark** against `1e18`; it re-runs on every hop and every endpoint that reads the base
(`_readBasePriceOrHalt`), but it says nothing about any other asset's peg. A per-asset peg breaker
would need a new field on `Asset` and is on the roadmap rather than in the deployed contracts, so do
not design around one today: the controls that exist for a non-base leg are the ref band, the
staleness and confidence gates, and `haltAsset`.

## 8. Guards

- `MAX_DEPTH = 4`, so `MAX_PATH_LENGTH = 2 * MAX_DEPTH + 1 = 9` nodes and 8 legs (section 1).
  `Err.DepthExceeded` and a cycle error are live reverts, not decoration.
- `validateAnchor` walks to the root with **both** an explicit `current == asset` cycle check **and**
  an unconditional step cap. A disconnected 2-cycle has finite walk length and never reaches the root,
  so a depth counter alone does not catch it.
- **The reference band is what generalizes, not the parity halt.** `PoolIOLib.priceBandGuardPath`
  loops the interior hops and runs `priceBandGuard` on each one except the base, and
  `PoolIOLib.priceBandGuard` runs on both endpoints, so every priced node of the path carries an
  agreement check. The parity halt does **not** generalize: `_readBasePriceOrHalt` is gated on
  `hop == $.baseToken` (`Pricing.sol`), so it tests the **base mark** against `1e18` at
  `BASE_DEPEG_HALT_BPS = 500` and tests nothing else, however deep the path. The base is skipped by
  the path guard precisely because the parity halt already covers it. Neither check is a
  parent-depeg breaker: the ref band is an agreement check and cannot express a peg test
  (section 7.1).
- **Coverage toll stays terminal-only** at any depth (section 2). Per-leg tolling would be a bug.
- **Hub κ is output-only**, not keyed off because the token is the base. A walled asset may be a
  parent. κ=0 is forbidden on every listed asset including the hub ([Invariants §I-9](/docs/1-1-8-invariants#i-9-coverage-toll-is-charge-only-and-terminal-only)).

## 9. Three defects a multi-leg path can carry, and where each is closed

Depth beyond 1 is safe only if three failure modes are closed, each in a different place and none by
restricting the topology: a basis error, a write-path error and an aggregation error. A flat roster
exhibits none of them only because it has no interior leg to exhibit them on.

- **Reciprocity basis (safety, extractable).** If a downward leg inverts the rate but not the skew
  multiplier `m`, a closed cycle returns `m^2` and leaks `m^2 - 1` per cycle, scaling with the skew
  actually loaded and with the cycle length. Closed by **condition C1** (section 4): one canonical
  integer per edge, direction expressed as multiply versus divide, and skew never re-applied after an
  inversion. Enforce it in code, not in prose.
- **Oracle-push bias (safety).** If interior, skew-loaded mids were written back into the internal
  feed, a trader could bias a stored mark at zero inventory cost by choosing direction. Closed by
  keeping leg prices read-only: nothing on the pricing path writes a mark. It must stay that way.
- **Interior-leg under-charge (revenue).** An interior leg that contributes nothing to the path fee is
  free routing through a node whose depeg risk the pool is carrying. Closed by fee composition
  (section 5): every leg a path crosses, interior included, pays into the path floor, and the interior
  leg's own floor is the fence of section 3.2. Charging impact instead would not close it, and would
  break the round-trip identity.

Two properties are sometimes mistaken for defects and are not. **Route path-dependence** cannot
arise: in a tree the path between two nodes is unique, so there is no decomposition for a quote to
depend on. **Wrapper feeds** are a per-pair data question, not a structural limit: an anchored pair
whose feed is mapped to the underlying (so that `cbBTC/WBTC` reads identically 1.0) carries no
information and must not be listed as an edge until NX Rates serves it as a first-class series.
Section 7 states that contract.

## 10. Implementation status

**Depth up to 4 is the design and it is built.** `AnchorTreeLib.MAX_DEPTH = 4` and
`MAX_PATH_LENGTH = 9` are live constants (`AnchorTreeLib.sol`);
`validateAnchor` walks to the root under both an explicit cycle check and an unconditional step cap.
Pinned by the anchor-validation tests:

- depths 2, 3 and 4 accepted;
- a direct depth-5 write rejected `DepthExceeded`;
- the 8-leg / 9-node worst case;
- the detached 2-cycle that only the step cap terminates.

**The depth bound is enforced at quote time, not as a storage invariant.** `validateAnchor` bounds
only the node being written. Re-parenting a node that already carries descendants lengthens every
chain beneath it and revalidates none of them, so an admin re-anchor can leave a depth-5 asset in
storage. That is caught fail-closed by the router: `findRoutingPath` re-walks under its own bound,
so `getSwapQuote`, `swap`, cross-asset `withdrawTo` and `swapLiability` all revert `InvalidPath` for
any endpoint deeper than `MAX_DEPTH`, and an over-deep tree can never price. Assets that remain at
depth 4 or less after the re-parent keep quoting normally, and the over-deep asset's LPs can still
exit through the same-asset `withdraw` path, which does not route. `collapseAnchor` moves one way
toward the root and cannot deepen anything; `setBaseToken` is depth-preserving on factory pools,
because the roster completeness scan forces every child of the old base into the `spokes` array and
re-anchors it to the new base at depth 1. The deployment scripts stand up a real tree in three stages, `addAnchorFeeds()`
→ `requestAnchorTree()` → `executeAnchorTree()`, and a local test-chain run formed depth 2 and read it back,
formed depth 4, and had depth 5 rejected.

**Flat or deep is configuration, not a build gap.** There is no `anchorTreeEnabled` flag: the `anchor` column in a pool's risk-param file **is** that pool's topology (§1.1). Three things gate the first fill of a deep edge:

| Gate | Requirement |
|---|---|
| The interior-leg pricing law (section 3) independently audited | must pass before the first fill |
| The cross pair served by NX Rates as a first-class series with its own $\sigma$ and its own push trigger | registration alone is not enough: a pair registered in `cexs.cross_pairs` but excluded from materialization has no live mark and no sigma |
| The indexer reading `quoteUnit` from chain instead of re-deriving denomination from the feed descriptor | required before any deep leg: UOA is legal only while `anchor == baseToken` |

None of the three is a contract gap. `PoolConfig` already refuses a deep leg that is not
`quoteUnit = 0`, and the deploy scripts already stand the tree up in three stages.

Still open in the contracts themselves:

| Item | State | Citation |
|------|-------|----------|
| Parent-depeg breaker (`absFeedId` or a split `refFeedId`) | field does not exist | section 7.1 |

Denomination and re-anchoring are settled: `OracleConfig` carries `uint8 quoteUnit`
(`QUOTE_UNIT_ANCHOR = 0`, `QUOTE_UNIT_UOA = 1`,
`PoolConstantsLib.sol`), and there is no untimelocked writer for an anchor.
Anchor and oracle config travel as one payload through `requestOp(..., UPDATE_ANCHOR, ...)` → `executeAnchorUpdate`
at the `CRITICAL` base-migration tier (`requestOp` UPDATE_ANCHOR and executor `executeAnchorUpdate`, both on `Admin.sol`), with a guardian
`collapseAnchor` that may only move a leg toward the root and halts it in the same write (`Admin.sol`).

### 10.1. Admin gating for a re-anchor

- **Timelocked at the base-migration tier.** Re-anchoring is a re-rooting of the subtree, so it
  queues at the `CRITICAL` tier (`requestOp(..., UPDATE_ANCHOR, ...)` → `executeAnchorUpdate`).
- **Atomic with the oracle config**: one payload `{token, anchor, OracleConfig}`, never separable.
  Re-anchoring X to P while X's feed is still attested in base units misprices the leg by the
  parent's price (roughly 3500x on stETH) and drains the reserve in one block.
- The guardian emergency path (`collapseAnchor`) may only re-anchor **toward the root**, and halts
  the leg in the same write. That direction always fails safe: the root feed always exists, and
  collapsing the tree only lengthens paths, which raises the summed fee.
- A partially re-anchored tree routes but prices differently mid-flight. Move one asset per queued
  payload, or set `HALT_MASK` on the subtree first.

## 11. Vocabulary

`tree`, `path`, `depth`, `anchor`, `LCA`, `interior`, `edge`, `leg` all describe the **intra-pool**
anchor topology and are accurate.

`route` and `hop` mean something else and are reserved for the **inter-pool** axis: BTR has no
on-chain router, so a cross-pool trade arrives as several `Swapped` logs and the collector indexes it
with a `hop_index`. Do not use `route` or `hop` for legs inside one pool.

`depth` also names the liquidity depth axis of the spline (`startDepth`, `endDepth`). That is an
unrelated homonym and is never the tree depth. A third sense, the impact **denominator**, is the
leg's raw reserves (`depth = reserves == 0 ? 1 : reserves` in `Pricing.quoteSwap` and
`_priceEdgeHop`). There is no "effective" or "virtual" depth: the denominator is the balance, so
those two senses need no further disambiguation.

## 12. One call, not two

A composite swap mutates only the two endpoint reserves. Splitting the same trade into two `swap`
calls makes the intermediate asset an endpoint: its reserves move, its coverage skew shifts the second
leg's mid, and the coverage toll and the `S/2` fee are each charged once **per call**, so the trader
pays both twice. Neither the output nor the resulting reserves match. Route an intra-pool trade as one
call.

## 13. Properties

- **Bounded quoting**: at most 8 legs and 9 nodes, no routing search, path resolved by two walks to
  the LCA.
- **Single price surface**: the path is unique, so the quote is path-independent by construction.
- **Correlated pairs quote tight regardless of base**: the fee, sigma and mark of `USDT -> USDC` come
  from that edge, not from the pool numeraire.
- **Unified accounting**: one base still backs every asset for measurement, without forcing every
  price through it.
- **Manipulation-checked, not depeg-checked**: per-edge skew plus an agreement band on every node of
  the path. A parent-depeg breaker would be a separate field and is roadmap, not shipped (section 7.1); do not claim the tree
  is depeg-safe until it exists.

---

## 14. Multi-pool routing (off-chain)

> Everything above prices one pool; this section composes several into one swap. Source of truth: the `@btr-protocol/sdk` router (`enumerateRoutes`,
> `quoteRoute`, `rankSwap`) and its depth-routing module. **Route selection is never on chain**:
> the winner is planned off-chain, then executed either as one `Pool.swap` per leg or as a single
> `Router.swap` carrying the whole path (see
> [Composability §2](/docs/5-1-4-composability#2-routing-through-the-router)).

### 14.1. Single-pool quote law

A pool settles every swap along the endpoints' anchor path; on flat rosters that is the
intra-pool spoke→base→spoke hop: one leg walk in, the toll/spread/fee settlement of §5, one leg walk
out. Deeper anchors add interior legs priced at mid ([§3](#3-leg-pricing)). The
off-chain replica is `quoteExactIn(state, in, out, amt)` in `@btr-protocol/sdk` - the same
quartic ladder, dispersion law ([Liquidity Shaping §6.2](/docs/1-1-2-liquidity-shaping#62-live-dispersion-formula)) and `_pathSpread` composition, so an
off-chain quote and the chain agree to integer rounding. A route of one leg is this call.

### 14.2. Two hops through a shared hub

Pools that list a common token (in practice the hub, USDC.b on every Arc core) compose into a
2-leg route: `tokenIn → hub` on pool A, `hub → tokenOut` on pool B. `enumerateRoutes` builds these
by intersecting each pool's token set (`sharedTokens`), excluding `tokenIn`/`tokenOut` themselves
as middles. Legs fill sequentially: leg 2 spends leg 1's **net** output (`quoteRoute`), so the
route's output is exactly what an atomic batch of the two swaps delivers, and the route's input
capacity is the first leg's reserve clip (`maxIn`) - downstream legs see less flow, never more.

Hub accounting follows §2: the hub leaves pool A (its coverage there falls, hub κ tolls the first
leg's output) and enters pool B (no toll - the wall is output-only).

### 14.3. Three hops, only when two hops cannot exist

If no pool pair shares a token, `enumerateRoutes` falls back to a bridging pool
`A --x--> M --y--> B` over every shared-token pair (x ≠ y). When one hub token is listed on every core, two hops always exist and this arm never fires; `rankSwap` still prices whatever is enumerated.

### 14.4. Ranking and splits: marginal water-fill

`rankSwap(pools, in, out, amountIn)`:

1. Quote every enumerated route at 100% size; rank by output (the `singles` inspection rows).
2. Split candidates must be **pool-disjoint**: the water-fill quotes each route against the pool's
   unmutated state, so two routes sharing a pool would each assume they alone draw its full depth -
   double-counting liquidity enough to pass the gas guard on a plan that then reverts or short-fills.
   At most `maxRoutes` (default 3) disjoint routes participate.
3. `waterFill` places `slices` (default 64) equal slices greedily on the route with the best next
   **marginal** output. Per-route output is concave (spline impact + inventory skew, capped flat at
   the reserve clip) and the spread floor is a proportional rate with no fixed activation cost, so
   equalising marginal price is optimal-in-the-limit; the greedy form is robust across the toll/skew
   kinks. A route past its clip stops winning slices (its marginal hits ~0); leftover amount is not
   routable (partial fill).
4. The split is kept only if it beats the best single route by more than `minGainBps` (default
   5 bps) - the gas guard for the extra leg.

### 14.5. Depth books compose with the same primitives

The rendered book is not a second model:

- **Direct pairs**: `aggregateDepthCurves` (`depthAgg.ts`) densifies each pool's `depthCurve`
  polyline onto a 1/2/5 price ladder and merges same-price buckets across pools. `depthCurve`
  vertices are pinned by the acceptance invariant `quoteExactIn(cumBase).grossOut == cumTok`
  (`aimm.ts`), so integrating a book side at `netPrice` reproduces the router's single-pool
  `amountOut` exactly (the rung's `price` is the pre-haircut skew price; `netPrice` carries the
  half-spread + toll the quote charges) - chart and quote are the same math at different sample
  densities.
- **Routed pairs** (no single pool holds both tokens): `aggregatePairDepth` (`depthRoute.ts`) first
  tries direct pools, then composes one synthetic `DepthCurve` per enumerated route
  (`aggregateRouteDepthCurves`): each leg's ask/bid ladder is chained input→output in the exact
  `quoteRoute` order - leg 2 spends leg 1's net output, haircuts (half-spread + coverage toll) folded
  into `netPrice` - and the composed curves merge mid-outward through the same assembler as direct
  books. Route-composed candidates are also kept pool-disjoint, for the same double-count reason as
  the splitter.

Consequence: the winning `rankSwap` plan and the DepthPanel's VWAP bands can never disagree about
price - both are integrals of the same quartic ladder with the same spread/toll haircuts, and the
routed book exists precisely when `rankSwap` finds a cross-pool route.
