---
title: "Oracle Price-Push Security"
description: "End-to-end trust chain for BTR marks: NX Rates provenance, on-chain k-of-n quorum, signer whitelisting ceremony, per-push guards, and why oracle-manipulation classes are bounded"
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-04"
publish: true
---
# Oracle Price-Push Security

> How BTR gets a trustworthy mark and why a compromised price source cannot drain a pool. Mechanical per-guard rules and the feed struct are in [Oracles](/docs/3-4-oracles); this page owns the trust chain around them.

---

## 1. Threat: oracle-manipulation classes

`EXTERNAL` (recommended) quotes any `IOracle` mark (on-chain adapter or push). `INTERNAL` is the cash-collateral $1.0$ peg helper only. See [Oracles §1](/docs/3-4-oracles#1-overview). This ceremony is the **NX Rates off-chain push**, one EXTERNAL option, not a protocol-wide prerequisite. A pool using EXTERNAL with a fully on-chain adapter carries none of the trust below. What follows is what a pool opts into for NX Rates' aggregated mark. The failure mode to defend is the one that has repeatedly drained oracle-priced venues:

- **Single-key compromise.** One stolen producer key pushes an arbitrary mark, the pool prices off it, an attacker swaps the drained side.
- **Gradual walk.** No single push looks unreasonable, but a compromised producer walks the mark many small steps until collateral is gone. A per-push sanity band alone does not stop this: the sum is unbounded.
- **Future-dated report.** A producer stamps a far-future timestamp that clears the replay guard once, then permanently freezes the feed at a stale price to be picked off.
- **Volatility understatement.** A producer signs $\sigma = 0$ to collapse the pricing spread, then round-trips against a spread-free mark.
- **Source manipulation.** No key is stolen and no producer misbehaves: an attacker moves a majority of the venues the composite is built from, and the quorum faithfully signs an aggregate of manipulated inputs. Nothing downstream can distinguish it from a real move: the guards below bound how fast that mark walks the pool, never whether it is right. The defenses are off-chain and economic; see [Security Overview §9.2](/docs/3-overview#92-oracle-limitations).

The design goal: track a real crash faithfully, never brick, and never let one (or few) compromised keys one-shot or slow-walk the mark.

---

## 2. Where prices come from

BTR does not acquire or aggregate prices. It consumes attested marks from high-frequency off-chain
providers and re-verifies them on chain. Two supply the roster today, **NX Rates** and **Pyth**, and
they do the same job: aggregate venue tape off chain at high frequency and attest the result. The
oracle keeper relays either through the same signed path, and they are used interchangeably, chosen
per feed on coverage and cadence rather than by rank.

This section describes the NX Rates path in detail because it is the one whose quorum BTR governs
on chain. **NX Rates** is a generic multi-exchange market-data provider that owns every venue
connection, aggregates a multi-venue mark plus Parkinson volatility and a confidence interval, and
co-signs the result. BTR is a **client** of it: it consumes signed quotes and re-verifies them
on-chain.

The quorum that signs a quote lives inside NX Rates. Each signer replica holds its own key on its own node. The replica serving a quote builds the blob from its live aggregated view, self-signs, and proposes it to its peers. **A peer countersigns only after re-validating every record against its own independent market view** (price tolerance, timestamp skew, and volatility/confidence understatement guards). Below the quorum of signatures, no quote is served at all. So a signed batch reaching the chain means k independent processes, holding k independent keys, each independently agreed this price was correct at this time.

Price acquisition, the aggregation method, the signing scheme, the `/v1/quote/signed` endpoint, blob field semantics, key management, and plan tier are **NX Rates internals**, documented and maintained by NX Rates (NXR); BTR docs do not reproduce them - see NXR's signed-quote specification (provided to integrators on request).

That provenance pointer is one-way: BTR docs point to NX Rates; NX Rates does not depend on BTR. What follows is on-chain BTR only: verification, the quorum registry, and the ceremony that governs it.

---

## 3. The on-chain quorum is the consensus

There is no separate consensus protocol between BTR and NX Rates. **The consensus is the on-chain signature check.** `ExternalOracle` holds the authoritative registry:

| State | Role |
|-------|------|
| `mapping(address => bool) signers` | The NX Rates attester whitelist. Only granted addresses count toward a quorum. |
| `uint8 signerThreshold` | $k$: the number of distinct granted signatures a batch must carry (2 in current deployments). |
| `uint8 signerCount` | $n$: live granted-signer count (`executeSignerGrant` / `revokeSigner` bookkeeping). |

`batchPushSigned(bytes blob, bytes sigs)` is the sole push path. Authorization:

1. `sigs` is $k$ concatenated 65-byte recoverable ECDSA signatures over one EIP-712 digest (the digest of $\text{keccak256}(\texttt{blob})$), with $k = \lvert\texttt{sigs}\rvert/65 \ge \texttt{signerThreshold}$.
2. Each signature is recovered against the same digest. Every recovered address must be a granted signer **and strictly greater than the previous recovered address**:

   ```solidity
   address rec = ECDSA.recoverCalldata(digest, sigs[off:off + 65]);
   if (rec <= prev || !signers[rec]) revert Err.NotAuth();
   prev = rec;
   ```

   `ExternalOracle.sol`. **Strict increase is the k-of-n deduplication check**, not a convenience: a repeated signature fails `rec <= prev` on its second appearance, and an unsorted set fails at the first inversion. Both are caught by the same comparison, with no set structure and no second pass. So $k$ accepted signatures prove $k$ **distinct** granted keys.
3. The stride is a fixed 65 bytes with no EIP-2098 compact form (`ExternalOracle.sol`). The signature count **is** the quorum claim, so its encoding must be unambiguous.
4. All signatures are verified **before any state write** (checks-effects).

The relayer (`msg.sender`, the keeper that lands the transaction) is **unpermissioned on the signed path**. Authority is in the signatures, not the sender. This decouples price authority (the NX Rates key set) from push liveness (any relayer can land the freshest signed blob), and it removes the single-signer failure mode: **one stolen key can push nothing on its own.**

V4 keeps that path as `pushSignedV4(blob, sigs)` and adds a second, cheaper one: `pushV4(blob)` pins `msg.sender` to the single relay named in a quorum-signed `SessionGrant`, valid for at most one hour and revocable by any one signer. The session path is where the routine cadence runs; the signed path is the permissionless fallback, so liveness never depends on the granted relay ([Oracles §10.3](/docs/3-4-oracles#103-session-grants-trust-model-and-bounds)).

Replay is defended by the per-feed monotonic `sourceTs`, never by signature uniqueness (a malleable $s$ still recovers the same signer; no on-chain state is keyed on signature bytes). See [Oracles §4.2](/docs/3-4-oracles#42-signed-push-path-batchpushsigned-k-of-n-quorum).

### 3.1. What the digest binds

The digest is `_hashTypedData(keccak256(BATCH_TYPEHASH || keccak256(blob)))`. Solady's EIP-712 domain separator commits to a name, a version, `chainId` and `verifyingContract = address(this)`. **The name and typehash are per generation**: V1 signs `BatchQuote(bytes32 blobHash)` under `BTR ExternalOracle` / `1`; the live V4 signs `BatchQuoteV4(bytes32 blobHash)` under `BTR ExternalOracleV4` / `1` (`ExternalOracleV4.sol`). Anything re-recovering a signature off-chain must use the domain of the instance that accepted the push (§7).

Both domain fields carry real weight for this threat model:

| Field | What it prevents |
|---|---|
| `chainId` | Cross-chain and post-fork replay. A batch signed for one chain's oracle is not valid on another deployment of the same contract, and solady recomputes the separator when `chainId` changes rather than serving a cached one. This becomes load-bearing the moment the same contract is live on more than one chain. |
| `verifyingContract` | **Cross-instance replay, which is the one that matters here.** A blob signed for the primary oracle cannot verify on the reference oracle, and vice versa, even when the two instances share a signer set. Without this binding the independent-reference cumulative bound (§5.1) would be defeated by pure message reuse rather than by key compromise: the same signatures would move both feeds in lockstep and the ref band would never open. |

The signature covers $\text{keccak256}(\texttt{blob})$, the exact packed calldata, so no record can be substituted, reordered or truncated after signing. V1's wire is an 8-byte header plus 22-byte ticker-keyed records resolved through the append-only `feedIdOf[tickerId]` map. **Wire v5, the live one**, is an 11-byte header plus positional entries — 5 B price, 5 B σ, 3 B confidence — each keyed by a `gi:u8` global index rather than a ticker, with sections in strictly-ascending `gi` ([Oracles §11.5](/docs/3-4-oracles#115-wire-v5)). An entry naming an unregistered lane is skipped fail-soft, not reverted; a descending or repeated `gi` fails the whole blob with `BadBlobHeader`.

---

## 4. The whitelisting ceremony

Governance of the signer set is deliberately asymmetric: **tighten now, loosen later.** Anything that could loosen price authority (adding a signer, lowering the quorum) is timelocked and vetoable. Anything that hardens or halts (revoking a signer, raising the quorum, pausing a feed) is immediate. A rogue owner cannot fast-add a malicious signer; a guardian can instantly retire a leaked one.

### 4.1. Genesis: no 1-of-1 bootstrap

The constructor installs the quorum atomically. It requires **3 to 16 distinct non-zero signers with `signerThreshold` between 2 and n** (`MAX_SIGNERS = 16`). A deployment can never pass through a single-signer or single-key state: there is no post-deploy "add the second signer" window during which one key is authoritative.

```solidity
// V4 (live). NxrSignerSet carries the genesis bounds for every generation.
constructor(address ac_, address[] initialSigners_, uint8 signerThreshold_, bool express_)
// reverts unless: 3 <= initialSigners_.length <= 16, all distinct + nonzero, 2 <= signerThreshold_ <= length
```

There is no `maxRelayLagSecs` on V4. The absolute past bound is the contract constant `MAX_RECON_AGE` = 6 h, shared by every feed and applied on both the push and the read side; `express_` is false on every deploy path ([Oracles §11.8](/docs/3-4-oracles#118-express-mode)).

`ac_` is an existing `AccessControl` address, so which governance root an oracle answers to is a deploy-script decision. The primary oracle on a chain shares that chain's `AccessControl` with the pool fleet; a **reference** oracle should not (§4.6).

### 4.2. Adding a signer (timelocked, loosening)

Adding price authority is a loosening, so it cannot take effect in the same transaction or block as the request:

1. Off-chain: generate a **standalone key** on its own node (independent failure domain, not derived from an existing signer).
2. `requestSignerGrant(signer)` (owner-only) queues one pending grant. Only one grant may be pending at a time, so a compromised owner cannot queue-farm many candidates that guardians must veto one by one.
3. Wait out the `BASE` tier delay.
4. `executeSignerGrant()` (owner-only) inside the grace window. Execution revalidates membership and the hard cap, because emergency revocations may have changed the live set while the request waited.
5. Any time before execution, **guardian or owner** may `cancelSignerGrant()` (veto, including an expired request).

### 4.3. Removing a signer (immediate, hardening)

`revokeSigner(signer)` is callable by **guardian or owner** and takes effect immediately: the fast removal of a leaked NX Rates key. Revoking below `signerThreshold` is deliberately allowed. Dropping under $k$ halts pushing (no quorum can form), which is the fail-safe response to a suspected compromise: feeds go stale and the pool fail-closes. A halt is never a loosening, so it needs no timelock.

### 4.4. Changing the quorum

| Direction | Path | Timing |
|-----------|------|--------|
| **Raise $k$** (`setSignerThreshold`, $t >$ current, $t \le$ `signerCount`) | Owner-only | **Immediate** (hardening) |
| **Lower $k$** (`requestSignerThresholdDecrease` → `executeSignerThresholdDecrease`, floored at 2) | Owner-only, one pending at a time | **Timelocked**, guardian/owner cancellable |

A decrease can never go below 2. Execution revalidates that the target is still a strict decrease and reachable by the live set.

### 4.5. Ceremony parameters

| Constant | Value | Meaning |
|----------|-------|---------|
| `MAX_SIGNERS` | 16 | Hard cap on $n$. |
| min signers / min $k$ at genesis | 3 / 2 | No 1-of-1 or 2-of-2 bootstrap. |
| Signer-governance delay | The `BASE` tier of `AccessControl.GOV_DELAYS()`, read into `ExternalOracle.DELAY_BASE` at construction ([schedule](/docs/3-1-access-control-roles-emergency-powers#5-timelock-parameters)) | Delay on grant and quorum decrease. |
| `SIGNER_GOV_GRACE` | 7 days | Execution window after maturity; then the request must be cancelled before re-queuing. |

### 4.6. Independent reference: the deploy disjointness preflight

The cumulative bound (§5) only works if the reference oracle is **operationally independent** of the primary. Address inequality is the on-chain floor (`PoolConfig.validateOracleConfig` requires `refPrimary != primary`).

**Signer disjointness is not implemented.** The only shipped preflight is a deploy-time check that the reference address differs from the primary and holds code. Nothing checks that the two instances have disjoint keys, admins or owners.

**Required configuration.** The reference oracle takes a signer set disjoint from the primary's, and its own `AccessControl`, which is the one place a second governance root on a chain is correct, precisely because the reference exists to police the primary. Sharing either is configurable and advised against: it collapses the two quorums below into one.

**Per-deployment status.** This is a property of a deployment, not of the protocol, so it must be checked per chain. **Arc shares a signer set today** for operational simplicity, and the cumulative bound does not hold there. Every chain added later inherits nothing from that: verify each one before relying on the bound, by comparing `SignerGranted` logs on both oracle addresses. Live addresses per chain: [Contract Addresses](/docs/2-1-contract-addresses).

**Operating obligation.** The reference is not push-and-forget: every armed spoke band fails closed once the reference goes stale, so a keeper instance must relay to the reference oracle on the same $\theta$/heartbeat discipline as the primary (separate `oracle-reference.*.toml` manifest, its own address-bound blob - the EIP-712 domain means one oracle's blob can never verify on the other). A parked reference oracle bricks spoke swaps after TTL.

**Consequence, stated plainly.** Where a deployment shares a signer set, one compromised k-of-n quorum signs both the mark and the reference, so the `refBand` walks in lockstep and never trips.

Note what this does and does not concede. The `verifyingContract` binding (§3.1) already prevents the *same signed blob* from moving both feeds: the attacker must produce a second, separately signed batch for the reference oracle. Sharing a signer set means they can, at the same key-compromise cost. Disjointness is what makes the second batch require a **second, independent** compromise. Address inequality buys nothing here; key and admin separation is the whole property.

`PoolConfig.validateOracleConfig` (`PoolConfig.sol`) enforces what a contract can: an armed `refBandBps` requires a non-zero `refFeedId`, a non-zero `refPrimary`, `refPrimary != primary`, and a successful `getFeed` on both. It cannot inspect signer sets, so operational independence stays a deployment obligation and belongs in the launch checklist, not in the contract.

---

## 5. How a push is guarded (defense in depth)

A verified quorum is necessary but not sufficient. Even a legitimately-signed batch, or a batch from a compromised quorum, passes through a fail-closed guard chain before it writes a mark. Each guard maps to a §1 vector. Exact rules and formulas: [Oracles §4.2](/docs/3-4-oracles#42-signed-push-path-batchpushsigned-k-of-n-quorum) and [§8.3](/docs/3-4-oracles#83-deviation-bounds).

| Guard | Bounds which vector | One-line rule |
|-------|--------------------|----------------|
| **k-of-n quorum** (§3) | Single-key compromise | $k$ distinct granted signatures per batch, or the push reverts. |
| **Monotonic `sourceTs`** | Replay / reorder | The reconstructed source second must strictly advance, **per slot** on V4 (8 lanes share one clock); the timestamp is the nonce. A slot that fails it is stepped over silently, with no event. |
| **Future-dated bound** (`SOURCE_TS_FUTURE_SKEW_SECS = 5`) | Future-dated freeze | Reject a `sourceTs` leading wall-clock by more than 5 s, which would clear the monotonic guard once and then freeze the feed forever. |
| **Absolute freshness** (`MAX_RECON_AGE` = 6 h, constant) | Withheld stale blob | Reject a blob whose reconstruction is older than the bound, on the read side as well as the push side. V1's per-instance `maxRelayLagSecs` does not exist on V4. |
| **One write per slot per source-second** | Batched walk | A slot accepts at most one write per source second — not per block. Several pushes may land in one block if their source seconds differ. A repeated or descending `gi` inside a section fails the blob closed. |
| **Volatility-adaptive band** | Per-push spike | A push may move the mark at most $10\,d_{max}$, and normally far less: $Z = 6$ standard deviations of Brownian motion over the attested source-time gap. Formula and terms: [Oracles §8.3](/docs/3-4-oracles#83-deviation-bounds). |
| **$\sigma$ floor** | Volatility understatement | Stored $\sigma$ is floored at the realized $\lvert\Delta p\rvert/p$, so any move forces a proportional spread; a signed $\sigma = 0$ cannot buy a spread-free round trip. On V4 the floor runs only where the σ word is already loaded — the band slow path, or a slot the blob carries σ entries for. |
| **Independent reference band** (`refPrimary` + `refBandBps`) | Gradual walk (cumulative) | Halt swaps once the mark diverges from a separately-keyed reference by the band ([Depeg Halt §2.4](/docs/3-5-depeg-halt#24-per-asset-price-band-depeg-guard-for-spokes)). |
| **Base-token depeg halt** | Numeraire depeg | Halt the hub when the base mark leaves parity by the halt band ([Depeg Halt §2.2](/docs/3-5-depeg-halt#22-halt-threshold)). |
| **Guardian fast-freeze** | Any suspected incident | Guardian or owner `pauseFeed` or `updateFeed` (tighten-or-equal on band **and** ttl), both immediate ([Access Control & Roles §2](/docs/3-1-access-control-roles-emergency-powers#2-guardian-surface-safe-direction)). Next release: `pauseFeed` is fail-closed on **release** as well — it clears the lane and anchors the frozen mark, so the feed reads DEAD through the pause and the re-entry push is banded over the real gap instead of over one cadence ([Oracles §8.1](/docs/3-4-oracles#81-manipulation-resistance)). `updateFeed`'s only inverse is the owner-timelocked widen (§5.1). |
| **$\sigma$ ceiling** | Overflow / absurd vol | Every stored $\sigma$ is capped at `MAX_SIGMA_PBPS`, on both the signed sample and the realized-move floor. |

### 5.1. Per-push band vs cumulative band

The signatures authorize **authenticity, not magnitude.** Two independent magnitude bounds run in series.

**Per-push (sanity).** The volatility-adaptive band lets a push move at most a $6\sigma$ Brownian step over the attested source-time gap, and never more than $10\,d_{max}$ whatever $\sigma$ says. This bounds a single compromised push to a monitorable step, never a one-tx drain.

**`maxDeviation` names the floor, not the maximum.** The band's real ceiling is exactly $10\,d_{max}$, so at the shipped stable floor of $d_{max} = 50$ bps a single push may move a stable's mark by up to **500 bps**, the entire width of the base parity band ([Depeg Halt §2.2](/docs/3-5-depeg-halt#22-halt-threshold)). Read the field as "the band's base term" and size it against the 10× ceiling. This interaction is why the two numbers must be chosen together rather than independently.

**A wedged feed self-clears, up to that ceiling.** The source-time gap grows while a feed is quiet, so the band widens with it and a feed that fell behind during an outage clears itself once the true move fits. Past $10\,d_{max}$ it does not: the band rejects the very push that would close the gap, `updateFeed` is tighten-or-equal for the owner as well as the guardian, and `registerFeed` refuses an existing id.

**Shipping in the next release, that wedge has an in-contract release.** `requestFeedWiden(feedId, maxDeviationBps, ttlSecs)` → `BASE` delay → `executeFeedWiden(feedId)`, both owner-only, with a guardian-or-owner `cancelFeedWiden` veto. Two properties matter for this threat model. First, **it loosens nothing by default**: the execute clears the lane and the rebias band anchor, so the next push is unbanded whatever the gap was, and passing the live band and ttl back verbatim is a valid pure release. Second, **a tighten always wins**: the request snapshots both fields and the execute reverts `InvalidState` unless both still match, so a guardian tighten taken during the delay voids the pending widen instead of being undone by it. Until the release lands the fallback stays a fresh oracle deployment plus a `BASE`-tier `UPDATE_ORACLE` repoint per affected leg — 37 legs on Arc today. Mechanics and the one-batch-per-slot operator rule: [Oracles §8.3](/docs/3-4-oracles#83-deviation-bounds).

**Cumulative bound.** A per-push band alone does not bound the total excursion: a compromised producer can walk the mark in many in-band steps. The **independent reference feed** closes that. `refPrimary` points the depeg band at a separately-keyed oracle instance (§4.6); the pool halts once the mark diverges from the reference past `refBandBps`. This is magnitude-based and push-rate-independent: no number of small in-band steps evades it, and a reference co-signed by the same quorum would bound nothing.

Given disjoint signer sets, walking the mark to drain a pool takes **two simultaneously-compromised independent quorums** (primary and reference), not one, and even then only within the reference band before swaps halt. Where a deployment shares a set (Arc today), that reduces to one quorum.

---

## 6. Signature scheme rationale (k ECDSA, not aggregation)

The quorum is $k$ **concatenated** 65-byte ECDSA signatures over one shared digest, deliberately not a BLS or Schnorr aggregate. On BSC this is the correct trade:

- **Aggregation is a gas regression on BSC.** BLS verification needs a BN254 pairing; Schnorr (Scribe-style) verification does an on-chain per-signer public-key SLOAD (~5,100 gas/signer) plus an off-chain nonce round, exceeding independent ECDSA recovery (~4,040 gas/signer). At the sizes this contract can express (`MAX_SIGNERS = 16`, and 2-of-3 in practice), $k$ plain `ecrecover` calls are cheaper and simpler than any aggregate.
- **It keeps add-a-signer trivial.** Adding a signer is a registry write (§4.2). Schnorr/BLS aggregation with a fixed committee key would turn every membership change into a key-ceremony re-share, which is exactly the flow the timelocked-registry design avoids.
- **It keeps the sub-50 ms quorum path.** Concatenated ECDSA needs no interactive nonce round between signers. Each replica signs the shared digest independently and the relayer sorts and concatenates. This matches the NX Rates quorum latency target.
- **The quorum cost amortizes once per batch.** All $k$ signatures cover the same digest (the hash of the whole blob), so a batch of $N$ feeds pays the k-signature verification once, not per feed. Packed 22-byte records (header carries the shared `sourceTsMs` once) keep the marginal per-feed cost minimal.

The gas comparison above is measured on BSC; the conclusion is expected to hold on the other EVM targets but has not been re-measured per chain. The scheme is kept swappable: the contract treats `sigs` as opaque bytes over an EIP-712 digest, so a future move to aggregation (should a large fixed signer set ever justify it) does not change the feed layout or the consumer path. Byte-exact wire format: `decodeBlob` in `@btr-protocol/sdk` plus `ExternalOracle.batchPushSigned`.

---

## 7. Transparency: verify it yourself

Nothing in the trust chain is taken on faith from a BTR server. The **Oracle Transparency page** in the BTR front-end reconstructs and re-verifies the whole model **client-side**:

- **Signer roster.** Rebuilt from `SignerGranted` / `SignerRevoked` logs, then cross-checked against `signers(addr)` RPC reads before display. Pending timelocked governance (a queued grant or quorum decrease) is shown with its ETA, so a loosening is visible before it lands.
- **Quorum badge.** The live `signerThreshold` of `signerCount`, read on-chain.
- **Feed table.** Mark, $\sigma$, confidence, age, `maxDeviation`, and freshness/paused status, straight from `getFeed()`.
- **Push log.** Recent push transactions fetched from a public block explorer, decoded from calldata, and their signatures **re-recovered in the browser** against the on-chain granted-signer set — under the domain of the instance that accepted them (`BTR ExternalOracleV4` / `1` and `BatchQuoteV4` on the live tiers, §3.1). The green "k/n verified" badge is computed locally; a lying explorer cannot forge a passing proof. V1 emitted nothing, so transparency there is calldata plus `getFeed()` state plus governance logs; **V4 emits** `SlotsPushed(seq, sourceTsDs, acceptedMask, blobHash)` on both push paths and `LanesSkipped(slotId, laneMask)` whenever a lane fail-softs, so a V4 push log can be indexed from events and the accepted mask read directly. Only a session push carries a `msg.sender` bound to the granted relay; a signed push carries none.
- **Relayers seen.** Listed as informational only, labelled "relayer, not price authority": any address may land a signed batch.
- **Per-price redirect into NX Rates.** Each decoded record deep-links to the NX Rates public verifier for the same `(symbol, sourceTs)` attestation, so an auditor can re-check the price against NX Rates' own signed-quote view. This is the same one-way citation as §2: BTR shows what it verified on-chain, NX Rates shows what it attested off-chain, and the two are checked to agree.

---

## 8. Related

- [Oracles](/docs/3-4-oracles): feed struct, per-guard mechanics, LVR/OEV, consumer reads
- [Feed Oracle](/docs/1-2-2-internal-oracle): oracle modes, FeedData, push API
- [Depeg Halt](/docs/3-5-depeg-halt): base-token and spoke depeg circuit breakers
- [Security Overview](/docs/3-overview): defense-in-depth, threat model, trust assumptions
- NX Rates (NXR): price acquisition, aggregation, and signing are documented by NX Rates and not reproduced here (one-way citation); see NXR's signed-quote specification (provided to integrators on request)
