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.
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, Parametrization §12.1, Invariants I-15 and Pool §7.
- 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 isMAX_PATH_LENGTH = 2 * MAX_DEPTH + 1 = 9nodes and2 * MAX_DEPTH = 8legs, 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. There is no route selection and no 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 does not read 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 is live code either way: once one anchor cell is filled it prices every transit.
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: the off-chain mark pipeline must serve the pair as a first-class series with its own σ and its own push trigger. Once the mark provider serves the cross, activation is one column change (write the parent into the child’s anchor) plus one queued op per leg: Admin.requestOp(pool, uint8(IPool.OpType.UPDATE_ANCHOR), bytes32(uint256(uint160(token))), abi.encode(anchor, oracleCfg)) -> the GOVERNANCE tier delay -> Admin.executeUpdateAnchor(pool, token). There is no anchor-tree batch entry point: the anchor and its oracle config travel as one payload per leg and are never separable. Every deep leg must carry quoteUnit = 0 (QUOTE_UNIT_ANCHOR) and a lane whose reference tier is attested in the anchor’s unit: a USD-denominated reference cannot bound an anchor-denominated mark.
2. Settlement: endpoints only
A swap mutates exactly two reserves:
The input leg’s reserves rise by amountIn and the output leg’s fall by amountOut
(PoolIOLib.settle; the fee slices and the liability side are in
Inventory Management §7).
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 (
PricingLib._covToll, called once fromPricingLib._settleQuote).dQ == 0on interiors, 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 value to the pool is depressed, and pricing the leg at the unskewed mark gives the trader that asset’s depeg risk at no charge.
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, PricingLib._legMid
(PricingLib.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 on the mark at the write, which is what lets both readings share one law: Liquidity Shaping §2.3.
3.2. The interior fence
An interior leg contributes max(minFee_i, fence_i) to the path floor
(PricingLib._priceInteriorLeg), where
is the fence, the leg’s mid swing, its curve’s span, its live
dispersion, its dispRefPbps, and = MAX_INTERIOR_SWING_PBPS, an internal
constant in PricingLib.sol.
The swing is peak-to-peak, and the two readings differ by a factor of 2. cap = 10,862 PBPS is a
1.0862%-of-mark total range, i.e. a 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 = (MAX_INTERIOR_SWING_PBPS + 1) / 2 = 5431, 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 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 (PricingLib._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.
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 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).
Swing above the cap reverts; it is never clamped (PricingLib._interiorMidAndFence). A clamp forks
the law: it bites on the interior leg only, the defect in 3.1. Failing closed cannot fork it.
The dispersion floor is bound at the write, against the cap this fence implies for the curve’s shape; the check reverts, never clamps. Cap formula, per-preset caps and write paths: Liquidity Shaping §6.5.
Cost: the fence is the leg’s live swing plus 0.5%. 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: 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 , and nothing bounds that ratio at write time; a shape whose edge sits at 5x its reference dispersion has ratio 10 against 2 for a unit-edge bell, 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 , so any shape below is extractable.
- Ratio-correct, sized per crossing. Fencing each crossing at its own live dispersion lets the cycle net : the attacker picks which crossing is the volatile one. Linear in , so bounding does not close it.
- Sized at each crossing’s live dispersion. With adaptive dispersion σ 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
uint16spread 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:
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:
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_eNever 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 for the clamped
quartic I-spline (NUQuarticLib), 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:
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 the sum over legs funds the path’s fence with no extra term.
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, , one sqrt at the end | _walkLegs, rooted at _pathSpread |
riskPath (confidence + staleness) | sum over all legs of | _walkLegs |
| upper bound on the composed spread | none: 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 the
off-chain mark pipeline 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
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. Trader protection is minAmountOut:
exact, caller-set, per trade, bounding the output. A protocol-side cap on the quote would instead
cap the pool’s own defense on the tape where the premium must be paid: a stale or high-CI leg is
when a wide quote is correct.
Full argument: Spread & Fees §3.1.
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.
6. Marks and denomination
The feed for asset X must be attested in units of
assets[X].anchor. The pool performs no re-denomination.
At depth >= 2 the base-denominated rule is dimensionally wrong, not just 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 = 0is the norm;QUOTE_UNIT_UOA = 1is the unit-of-account bridge, and a bridged leg’sminFeemust cover2 * (theta_child + theta_parent)(PoolConstantsLib.sol). ReadQUOTE_UNIT_UOAas the instruction “divide by the base mark”, not as a unit: it is legal only while the asset anchors directly to the base (PoolConfigLib.solrejects it otherwise), because dividing by the base price does not denominate against 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
PricingLib._legMarkAndFees, reusing the same gated, depeg-banded base read the path already performs. - The depeg gate is a reference band first. It is
OracleConfig.refBandBps, the tolerance between a leg’s primary mark and the same lane’s reference (R) tier, run on every interior node byPoolIOLib.priceBandGuardPathand on both endpoints byPoolIOLib.priceBandGuard. A per-asset depeg code (RiskConfig.flagsbits 12 to 15, band = code x 250 bps) adds a parity test against 1.0, meaningful where the mark is USD-attested or the pair is meant to trade at 1; the base carries its own parity band (BaseDepegged, 2,500 bps at code 0). An anchor-attested mark has no fixed unit to test against, so a same-unit comparison against a second attestation is the breaker there (section 7.1). The discretionary stop belongs to the guardian halt. - Multi-leg marks telescope algebraically. Residual is at most 1 ulp per leg.
7. Off-chain mark 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 store lane (
OracleConfig.lane, below 64;PoolFactory.setFeedLanemaps a feed id to it for readers), its own sigma, and its own push trigger withtheta_bpsderived from the cross’s realized volatility. - It is a quotient of one consistent USD vector (
DAI/USDoverUSDC/USD), which telescopes exactly, and still carries its own sigma and push trigger. - 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 lane’s reference tier carries the same unit as the primary, so an anchored pair references the anchored pair, never the absolute leg.
7.1. What the reference band is, and what it is not
OracleConfig.refBandBps is an agreement check between two attestations of the same quantity:
the lane’s primary (P) tier and its reference (R) tier in the chain’s one mark store. It is not a
peg test.
The band compares the primary mark against the reference mark in the same unit:
uint256 dev = pRaw > refP ? pRaw - refP : refP - pRaw;
if (dev * SC.BPS > refP * bandBps) revert Err.PriceOutsideRefBand(pRaw, refP);PoolIOLib.sol. The reference is not a second feed id or oracle address: OracleConfig is
{lane, mode, quoteUnit, refBandBps} and names one store lane, whose R tier is signed by its own
roster under its own EIP-712 domain. The band measures how far the two tiers disagree. It is a
manipulation cross-check, and it fails closed on a stale, dead or over-uncertain reference. Roster
disjointness between the tiers is a deployment obligation the chain cannot see.
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 series are not the same quantity, so their difference is not a deviation.
The lane’s R tier is attested in the primary’s own catalog unit, so an anchored pair is checked
against the anchored pair.
A parent-depeg breaker does not exist for an anchor-attested child. Bounding ETH’s peg from a
stETH/ETH primary would need the child-versus-USD attestation alongside the anchored primary, and
no field carries it. What does exist is narrower:
- the depeg code (
RiskConfig.flagsbits 12 to 15, stored inAsset.refBandBps, band = code x 250 bps) tests the primary mark against 1.0 and revertsPriceOutsideRefBand(mark, WAD). It runs inside the reference-band guard, so it needs a nonzero ref band. On aQUOTE_UNIT_UOAleg or an INTERNAL leg’s gate mark that is a USD peg test; on an anchor-attested leg it tests the child against its parent, which is a peg test only for a pair meant to trade at 1 and meaningless for a ratio such asstETH/ETH; - the base parity halt (
_readBasePriceOrHalt,BaseDepegged) tests the base mark against1e18at the base’s band (code x 250 bps, 2,500 at code 0). It re-runs on every hop and every endpoint that reads the base and tests no other asset’s peg.
For an anchor-attested child the controls are the ref band, the staleness and confidence gates, and
haltAsset.
8. Guards
MAX_DEPTH = 4, soMAX_PATH_LENGTH = 2 * MAX_DEPTH + 1 = 9nodes and 8 legs (section 1).Err.DepthExceededand a cycle error are live reverts.validateAnchorwalks to the root with both an explicitcurrent == assetcycle 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.priceBandGuardPathloops the interior hops and runspriceBandGuardon each one except the base, andPoolIOLib.priceBandGuardruns on both endpoints, so every priced node of the path carries an agreement check. The parity halt does not generalize:_readBasePriceOrHaltis gated onhop == $.baseToken(PricingLib.sol), so it tests the base mark against1e18at the base’s band (depeg code x 250 bps, 2,500 at code 0) and tests nothing else, however deep the path. The base is skipped by the path guard 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.
kappaCovBpsis floored on-chain on every listed asset including the hub (PoolConfigLib.requireNeverDepletable) (Invariants §I-9).
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 returnsm^2and leaksm^2 - 1per cycle, scaling with the skew 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 not defects. 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 the off-chain mark pipeline serves it as a first-class series.
Section 7 states that contract.
10. Implementation status
Depth up to 4 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_qe, 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.
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 the off-chain mark pipeline as a first-class series with its own and its own push trigger | registration alone is not enough |
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. PoolConfigLib already refuses a deep leg that is not
quoteUnit = 0.
Still open in the contracts themselves:
| Item | State | Citation |
|---|---|---|
| Parent-depeg breaker for an anchor-attested child | no field carries the child-versus-USD attestation | 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, ...) → executeUpdateAnchor
at the GOVERNANCE base-migration tier (requestOp UPDATE_ANCHOR and executor executeUpdateAnchor, 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
GOVERNANCEtier (requestOp(..., UPDATE_ANCHOR, ...)→executeUpdateAnchor). - 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 halt the subtree first (
haltAsset).
11. Vocabulary
tree, path, depth, anchor, LCA, interior, edge, leg all describe the intra-pool
anchor topology.
route and hop 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 indexer records 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 sense is
never the tree depth. A third sense, the impact denominator, is the leg’s raw reserves: Inventory Management §4.
12. One call, not two
A composite swap mutates only the two endpoint reserves. Splitting the same trade into two swap_qe
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.
- Correlated pairs quote tight regardless of base: the fee, sigma and mark of
USDT -> USDCcome 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.
Multi-pool route search is off-chain: Quotes & Routing §8.