Context
Fresh, complete data can still be manipulated: a thin single-venue asset (USTRY) is pushed on one venue and consumers price collateral off it. Multi-source consensus does not help a single-venue asset, and a fixed global percentage threshold fits no asset class (stablecoin 0.05% and memecoin 50% per bucket are both routine), freezes real crashes, and can be dodged by slow drift.
Decision
Anomaly response is a continuous confidence score plus a freeze policy on the closed-bucket surface, not a binary published/not-published decision on a fixed threshold. Formulas, factor shapes, thresholds and the freeze lifecycle are in oracle-manipulation-defense.md.
- Per-asset baseline. Per
(base, quote)pair, rolling 30-day robust statistics:return_medianandreturn_mad(MAD scaled by 1.4826, not sigma, because sigma inflates after the first attack and hides the next), plus typical source count and liquidity.z_score = abs(return_pct - return_median) / return_mad.return_madis computed at 1d, 7d and 30d, and the anomaly fires on the LARGEST z across windows holding enough samples (frog-boiling defence). - Confidence. Every published price carries
confidencein [0, 1], the normalised weighted geometric meanprod(factor_i ^ weight_i) ^ (1 / sum(weights))of seven factors: z-score, source count, source-class diversity, liquidity (log-saturating up to $1,000,000), cross-oracle agreement, triangulation agreement (weight 0.5, weight 0 when unchecked, never counted as a source) and baseline quality. The wire carriesconfidence_factors, the per-factor decomposition. - Freeze. The freeze fires only when all three hold:
confidence < 0.45 AND z_score > 5.0 AND source_count <= 10.45, not the original 0.10: at 0.10 the trigger was z of about 15, so the control was decorative.
- Per-surface policy (ADR-0018).
/v1/price(closed bucket) honours the freeze and serves the last-known-good withflags.frozen: trueand its originalobserved_at;flags.divergence_warningstays whatever the divergence service says, it is not forced./v1/price/tipand/v1/observationsignore the freeze: the live value and the raw per-source data are their contract. - Freeze lifecycle. Initial hold 30 minutes, or 10 for a pair with no corroborating lens (a lens that produced a reading, agreeing or not). At each expiry, if the condition still holds, extend 30 minutes, up to 4 times; after that escalate to an operator (P1) and stay frozen until an operator ends it. The ladder is per (pair, window) for 5m, 1h and 24h; the marker's presence is the single pair-level
flags.frozen. - Auto-unfreeze. From the end of the initial hold, two consecutive buckets with
confidence > 0.30ANDz_score < 3.0ANDrelease_corroborated(a lens reading from this bucket agrees with the bucket's own fresh price within 5%; a composite lens releases withinrelease_band_pct, see the detail page). A pair with no lens cannot auto-release. An escalated freeze never auto-releases. - Operator override.
stellarindex-ops freeze-unfreeze -reason ...; it clears the marker and stampsrecovered_atin the durable record. A bare RedisDELis not an override. - Bootstrap. A pair with no usable baseline publishes no
confidenceand has no Phase 2 freeze eligibility; the Phase 1 per-class percentage threshold ([anomaly.thresholds]:warn_pctandfreeze_pct, freezing only a single-source step) still applies to it. With a baseline, confidence is capped at 0.5 until baseline density reaches 28.5 days-equivalent, re-engaged below 27. - Rollout. Phase 1 per-class thresholds (transitional), Phase 2 statistical baselines, Phase 3 cross-oracle integration (
internal/divergence/).
Invariant
- The Phase 2 freeze must never fire on fewer than three signals:
confidence,z_scoreandsource_countare separate legs of one AND (the Phase 1 class-threshold rule and the triangulated-composite refusal are separate freeze paths). Enforced byTestPhase2FreezeFires_MissingOneSignalandTestPhase2FreezeFires_ConfidenceConditionIsNotVacuous. confidenceis the normalised combiner: the exponent is1 / sum(all seven weights),w_triis 0 when triangulation is unchecked, and a composite never feedssource_count. Enforced byinternal/aggregate/confidence/adr_parity_test.go.- A freeze is never released by calm alone: auto-unfreeze needs a corroborating lens that agrees with the candidate price, and an escalated freeze is released only by an operator.
- The freeze record is durable and fail-closed: Redis loss never releases a live freeze, a window's release never ends a sibling window's ladder, and ambiguity about ownership resolves to the longer hold. Enforced by the freeze and orchestrator tests under
internal/aggregate. - Freeze applies to the closed-bucket surface only; tip and observations never freeze.
Consequences
- No operator picks "the right percentage": the threshold is what 5 sigma of the asset's own MAD computes to, and one algorithm covers stablecoins to memecoins.
- Confidence is graded, so sophisticated consumers gate on it; the freeze is the safety net for consumers who do not read it. It does not defend against multi-source manipulation across several CEXes; that needs the Phase 3 cross-oracle reference.
- A freeze serves a stale last-known-good price, which is its own money bug (MNY-22); the short uncorroborated hold and the corroboration-gated release trade that against a false-freeze on thin books.
- A pair without a baseline publishes no
confidence; its absence is the signal consumers gate on. - Operators get alerts
stellarindex_anomaly_freeze_escalated(P1),stellarindex_anomaly_freeze_extension_rate(P3) andstellarindex_anomaly_freeze_active, and the runbook anomaly-freeze-engaged. - Alternatives rejected: permanent per-asset fixed percentages, a hard 503 on anomaly, Pyth-style range pricing as the default wire shape, always-publish-never-freeze, sigma instead of MAD, a single rolling window, and skipping Phase 1.
Evidence
internal/aggregate/freeze(lifecycle policy),internal/aggregate/orchestrator(freeze, composite reference, confidence),internal/aggregate/baseline(multi-window baseline),internal/aggregate/confidence(combiner).TestPhase2FreezeFires_CalibratedToADRZBandpins the freeze band;internal/aggregate/confidence/adr_parity_test.gopins the combiner and constants in the architecture page.- Design context: oracle-manipulation-defense.md; related ADRs 0010, 0017, 0018; migrations 0119, 0163.