feat(security): C-2 — oracle circuit-breaker pause fails window closed on vFDUSD collapse #22

Closed
opened 2026-08-08 04:40:18 +00:00 by PlasticDigits · 13 comments
PlasticDigits commented 2026-08-08 04:40:18 +00:00 (Migrated from gitlab.com)

Summary

Implement only the oracle-level pause / circuit-breaker portion of C-2 from audits/INTERNAL_KIMIK3_1786162831.md: when a vFDUSD economic collapse (or detected price decrease vs the monotonic on-chain rate) is recognized, the oracle path can halt swaps immediately, instead of waiting for window staleness (~6h) while the stale high rate enables a slow treasury drain.

Audit ref: C-2 (recommendation #1 only)
Explicitly out of scope for this issue: emergency rate reset / monotonic bypass; off-chain spot-vs-exchangeRateStored divergence alerter; docs-only risk disclosures (unless needed for the pause UX).


Current codebase

Component Path Behavior today
Oracle pause contracts/ust1-oracle ExecuteMsg::SetPaused Exists, governance-only; blocks UpdateRate when cfg.paused
Oracle state query StateResponse Returns rate / last_update_sec / day fields — does not surface paused
Window freshness contracts/ust1-window/src/contract.rs ensure_oracle_fresh Only age-checks last_update_sec vs max_oracle_age_sec; ignores oracle paused
Window pause ust1-window SetPaused Pauses deposits+withdraws for that window only
Policy ust1-common oracle_policy.rs Hard-rejects rate decreases (MonotonicViolation) — by design
Oracle service oracle-service On decrease, skips broadcast; cannot halt the window

Gap vs C-2 #1: Pausing the oracle today only stops further UpdateRates. The window keeps minting/redeeming at the last (pre-collapse) rate until the staleness budget expires — exactly the window an attacker needs for the ~rolling-limit/day drain.


Why this is needed

  1. Monotonicity correctly blocks marking the rate down, but leaves no fast on-chain kill switch that freezes window activity at the stale rate.
  2. Governance pausing only the window works, but ops may detect collapse via the oracle service first; the circuit breaker should live at the oracle trust boundary so all windows reading that oracle fail closed.
  3. Without fail-fast on oracle pause, C-2 remains a slow drain (~default 10k UST1/day) until someone notices and pauses each window.

Constraints / guardrails

  1. Scope lock: only pause/circuit-breaker wiring — no new “set rate down” path in this issue.
  2. Pause must be governance-controlled for unpause (avoid permanent DoS by a compromised operator if operator-trigger is added).
  3. If operator/service may trigger pause on detected decrease, require: governance unpause only; clear events; no silent auto-unpause.
  4. Window must fail closed on both deposit and withdraw when oracle is in circuit-breaker state (same error class or dedicated OraclePaused).
  5. Do not weaken monotonicity, daily cap, throttle, or rolling limits.
  6. Pausing oracle must not brick queries needed for monitoring (Config / State still readable).
  7. Prefer migrate-friendly changes (additive query fields / checks); avoid storage layout breaks without migrate tests.
  8. Document ops: when to pause (suspected vFDUSD/Venus/bridge collapse), who can pause, who can unpause.

Relevant files

  • contracts/ust1-oracle/src/{msg,contract,state}.rs — pause semantics / query surface
  • contracts/ust1-oracle/src/multitest.rs — pause ACL + interaction tests
  • contracts/ust1-window/src/contract.rs — query_oracle_state / ensure_oracle_fresh (or successor)
  • contracts/ust1-window/src/{error,msg}.rs — error + any query aggregation
  • contracts/ust1-window/src/multitest.rs — deposit/withdraw blocked when oracle paused
  • oracle-service/src/main.rs (optional) — call pause trigger if in scope
  • docs/DEPLOYMENT.md — emergency pause runbook

  1. Expose oracle paused status to the window (e.g. include paused in a query the window already uses, or have window query Config / a combined health query).
  2. In window deposit and withdraw: if oracle paused → revert immediately (do not wait for max_oracle_age_sec).
  3. Keep governance SetPaused as the authority for pause/unpause; optionally add a narrow operator TripCircuitBreaker / SetPaused if product wants oracle-bot reaction to detected decrease — unpause remains governance-only.
  4. Emit clear attributes (action=set_paused, paused=true) already present; ensure window error is operator-visible.
  5. Update DEPLOYMENT emergency section: pause oracle (circuit breaker) vs pause window (local) — prefer oracle pause to freeze all readers.

Acceptance criteria

  • When oracle paused=true, window deposit and withdraw both revert without waiting for staleness.
  • When oracle paused=false and rate fresh, swaps behave as today.
  • Governance can pause/unpause oracle; unauthorized callers cannot unpause (and cannot pause unless explicitly designed).
  • UpdateRate remains blocked while paused (existing behavior preserved).
  • No emergency rate-decrease / reset implemented in this issue.
  • Tests cover pause→window halt→unpause→resume; docs updated for ops.

Test plan (all paths)

  1. Oracle pause blocks UpdateRate (existing) — still green.
  2. Oracle paused → window deposit reverts immediately (rate may still be “fresh” by age).
  3. Oracle paused → window withdraw reverts immediately.
  4. Oracle unpaused + fresh rate → deposit/withdraw succeed.
  5. Oracle unpaused + stale rate → still OracleStale (unchanged).
  6. Window paused + oracle unpaused → window pause still wins for that window.
  7. Unauthorized SetPaused / unpause → Unauthorized.
  8. Optional operator trip: operator can pause (if implemented); operator cannot unpause; governance can.

Test plan (attack / hack / abuse vectors)

  1. Collapse drain race: attacker deposits/withdraws at stale high R after Venus/FDUSD collapse — oracle pause must stop both directions before rolling limit is the only brake.
  2. Malicious operator DoS (if operator can trip): pause freezes markets; unpause requires governance — document; test operator cannot unpause.
  3. Governance griefing: pause forever — accepted centralized risk (same as window pause); no code path for third-party unpause.
  4. Pause bypass via direct oracle State trust: window must not use a code path that ignores paused.
  5. Query spoofing in multitest: ensure checks use on-chain oracle config/state, not a client-supplied flag.
  6. Partial pause (deposits only): not in scope — both directions fail closed (symmetric with economic risk).

Verification criteria

  • Multitest/integration prove window fail-closed on oracle pause with a still-fresh last_update_sec.
  • Mainnet migrate (if needed) applied; governance runbook lists oracle circuit-breaker steps.
  • C-2 remaining items (rate reset, spot alerter) tracked separately — not required to close this issue.
## Summary Implement **only** the oracle-level pause / circuit-breaker portion of **C-2** from [`audits/INTERNAL_KIMIK3_1786162831.md`](../audits/INTERNAL_KIMIK3_1786162831.md): when a vFDUSD economic collapse (or detected price decrease vs the monotonic on-chain rate) is recognized, the oracle path can halt swaps **immediately**, instead of waiting for window staleness (~6h) while the stale high rate enables a slow treasury drain. **Audit ref:** C-2 (recommendation #1 only) **Explicitly out of scope for this issue:** emergency rate reset / monotonic bypass; off-chain spot-vs-`exchangeRateStored` divergence alerter; docs-only risk disclosures (unless needed for the pause UX). --- ## Current codebase | Component | Path | Behavior today | |-----------|------|----------------| | Oracle pause | `contracts/ust1-oracle` `ExecuteMsg::SetPaused` | **Exists**, governance-only; blocks `UpdateRate` when `cfg.paused` | | Oracle state query | `StateResponse` | Returns `rate` / `last_update_sec` / day fields — **does not surface paused** | | Window freshness | `contracts/ust1-window/src/contract.rs` `ensure_oracle_fresh` | Only age-checks `last_update_sec` vs `max_oracle_age_sec`; **ignores oracle paused** | | Window pause | `ust1-window` `SetPaused` | Pauses deposits+withdraws for that window only | | Policy | `ust1-common` `oracle_policy.rs` | Hard-rejects rate decreases (`MonotonicViolation`) — by design | | Oracle service | `oracle-service` | On decrease, skips broadcast; **cannot** halt the window | **Gap vs C-2 #1:** Pausing the oracle today only stops further `UpdateRate`s. The window keeps minting/redeeming at the last (pre-collapse) rate until the staleness budget expires — exactly the window an attacker needs for the ~rolling-limit/day drain. --- ## Why this is needed 1. Monotonicity correctly blocks marking the rate down, but leaves no fast on-chain kill switch that freezes **window** activity at the stale rate. 2. Governance pausing only the window works, but ops may detect collapse via the oracle service first; the circuit breaker should live at the oracle trust boundary so all windows reading that oracle fail closed. 3. Without fail-fast on oracle pause, C-2 remains a slow drain (~default 10k UST1/day) until someone notices and pauses each window. --- ## Constraints / guardrails 1. **Scope lock:** only pause/circuit-breaker wiring — no new “set rate down” path in this issue. 2. Pause must be **governance-controlled** for unpause (avoid permanent DoS by a compromised operator if operator-trigger is added). 3. If operator/service may **trigger** pause on detected decrease, require: governance unpause only; clear events; no silent auto-unpause. 4. Window must fail closed on **both** deposit and withdraw when oracle is in circuit-breaker state (same error class or dedicated `OraclePaused`). 5. Do not weaken monotonicity, daily cap, throttle, or rolling limits. 6. Pausing oracle must not brick queries needed for monitoring (`Config` / `State` still readable). 7. Prefer migrate-friendly changes (additive query fields / checks); avoid storage layout breaks without migrate tests. 8. Document ops: when to pause (suspected vFDUSD/Venus/bridge collapse), who can pause, who can unpause. --- ## Relevant files - `contracts/ust1-oracle/src/{msg,contract,state}.rs` — pause semantics / query surface - `contracts/ust1-oracle/src/multitest.rs` — pause ACL + interaction tests - `contracts/ust1-window/src/contract.rs` — `query_oracle_state` / `ensure_oracle_fresh` (or successor) - `contracts/ust1-window/src/{error,msg}.rs` — error + any query aggregation - `contracts/ust1-window/src/multitest.rs` — deposit/withdraw blocked when oracle paused - `oracle-service/src/main.rs` (optional) — call pause trigger if in scope - `docs/DEPLOYMENT.md` — emergency pause runbook --- ## Recommended direction 1. Expose oracle paused status to the window (e.g. include `paused` in a query the window already uses, or have window query `Config` / a combined health query). 2. In window deposit **and** withdraw: if oracle paused → revert immediately (do not wait for `max_oracle_age_sec`). 3. Keep governance `SetPaused` as the authority for pause/unpause; optionally add a narrow operator `TripCircuitBreaker` / `SetPaused` if product wants oracle-bot reaction to detected decrease — **unpause remains governance-only**. 4. Emit clear attributes (`action=set_paused`, `paused=true`) already present; ensure window error is operator-visible. 5. Update DEPLOYMENT emergency section: pause oracle (circuit breaker) vs pause window (local) — prefer oracle pause to freeze all readers. --- ## Acceptance criteria - [ ] When oracle `paused=true`, window deposit and withdraw both revert without waiting for staleness. - [ ] When oracle `paused=false` and rate fresh, swaps behave as today. - [ ] Governance can pause/unpause oracle; unauthorized callers cannot unpause (and cannot pause unless explicitly designed). - [ ] `UpdateRate` remains blocked while paused (existing behavior preserved). - [ ] No emergency rate-decrease / reset implemented in this issue. - [ ] Tests cover pause→window halt→unpause→resume; docs updated for ops. --- ## Test plan (all paths) 1. **Oracle pause blocks UpdateRate** (existing) — still green. 2. **Oracle paused → window deposit reverts** immediately (rate may still be “fresh” by age). 3. **Oracle paused → window withdraw reverts** immediately. 4. **Oracle unpaused + fresh rate →** deposit/withdraw succeed. 5. **Oracle unpaused + stale rate →** still `OracleStale` (unchanged). 6. **Window paused + oracle unpaused →** window pause still wins for that window. 7. **Unauthorized SetPaused / unpause →** Unauthorized. 8. **Optional operator trip:** operator can pause (if implemented); operator cannot unpause; governance can. --- ## Test plan (attack / hack / abuse vectors) 1. **Collapse drain race:** attacker deposits/withdraws at stale high R after Venus/FDUSD collapse — oracle pause must stop both directions before rolling limit is the only brake. 2. **Malicious operator DoS (if operator can trip):** pause freezes markets; unpause requires governance — document; test operator cannot unpause. 3. **Governance griefing:** pause forever — accepted centralized risk (same as window pause); no code path for third-party unpause. 4. **Pause bypass via direct oracle State trust:** window must not use a code path that ignores paused. 5. **Query spoofing in multitest:** ensure checks use on-chain oracle config/state, not a client-supplied flag. 6. **Partial pause (deposits only):** not in scope — both directions fail closed (symmetric with economic risk). --- ## Verification criteria - Multitest/integration prove window fail-closed on oracle pause with a still-fresh `last_update_sec`. - Mainnet migrate (if needed) applied; governance runbook lists oracle circuit-breaker steps. - C-2 remaining items (rate reset, spot alerter) tracked separately — not required to close this issue.
PlasticDigits commented 2026-08-08 04:52:40 +00:00 (Migrated from gitlab.com)

mentioned in commit f312188dd1f8a7c61dc32de5647a58f3e041293e

mentioned in commit f312188dd1f8a7c61dc32de5647a58f3e041293e
PlasticDigits commented 2026-08-08 04:52:50 +00:00 (Migrated from gitlab.com)

mentioned in merge request !39

mentioned in merge request !39
PlasticDigits commented 2026-08-08 04:53:26 +00:00 (Migrated from gitlab.com)

mentioned in merge request !40

mentioned in merge request !40
PlasticDigits commented 2026-08-08 05:10:48 +00:00 (Migrated from gitlab.com)

MR !40 verification (C-2 #1 / #22)

Verdict: PASS_WITH_NOTES — merging with deploy-order caveat.

AC (in-scope)

  • Oracle paused → window deposit/withdraw revert immediately (fresh rate)
  • Unpaused + fresh → normal behavior
  • Governance-only SetPaused; unauthorized rejected
  • UpdateRate blocked while paused (preserved)
  • No emergency rate reset
  • Multitest + DEPLOYMENT runbook

Tests (local)

cargo test -p ust1-oracle --lib — 9/9
cargo test -p ust1-window --lib — 17/17
cargo test -p ust1-integration-tests --lib — 8/8

Problems / follow-ups

  1. Migrate oracle wasm before window — new State.paused field; window upgrade against old oracle will fail deserialization.
  2. Mainnet governance pause smoke test still manual.
  3. Operator auto-trip on detected decrease remains out of scope (service skips UpdateRate only).
  4. No integration-suite case for oracle-pause path (multitest covers it).
  5. Prior pipelines only ran secret_detection.
## MR !40 verification (C-2 #1 / #22) **Verdict: PASS_WITH_NOTES — merging with deploy-order caveat.** ### AC (in-scope) - [x] Oracle paused → window deposit/withdraw revert immediately (fresh rate) - [x] Unpaused + fresh → normal behavior - [x] Governance-only SetPaused; unauthorized rejected - [x] UpdateRate blocked while paused (preserved) - [x] No emergency rate reset - [x] Multitest + DEPLOYMENT runbook ### Tests (local) `cargo test -p ust1-oracle --lib` — **9/9** `cargo test -p ust1-window --lib` — **17/17** `cargo test -p ust1-integration-tests --lib` — **8/8** ### Problems / follow-ups 1. **Migrate oracle wasm before window** — new `State.paused` field; window upgrade against old oracle will fail deserialization. 2. Mainnet governance pause smoke test still manual. 3. Operator auto-trip on detected decrease remains out of scope (service skips UpdateRate only). 4. No integration-suite case for oracle-pause path (multitest covers it). 5. Prior pipelines only ran `secret_detection`.
PlasticDigits commented 2026-08-08 05:12:13 +00:00 (Migrated from gitlab.com)

mentioned in commit c1d19c7329

mentioned in commit c1d19c7329ab881724554a5cd015e0adb22c957e
PlasticDigits commented 2026-08-08 05:12:29 +00:00 (Migrated from gitlab.com)

mentioned in commit deb4453da1

mentioned in commit deb4453da1246450800e3d593cbad939a8f9fc9e
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-08 05:12:29 +00:00
PlasticDigits commented 2026-08-08 06:01:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #26

mentioned in issue #26
PlasticDigits commented 2026-08-08 06:01:29 +00:00 (Migrated from gitlab.com)

mentioned in issue #27

mentioned in issue #27
PlasticDigits commented 2026-08-08 06:01:29 +00:00 (Migrated from gitlab.com)

marked as related to #27

marked as related to #27
PlasticDigits commented 2026-08-08 06:01:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #28

mentioned in issue #28
PlasticDigits commented 2026-08-08 06:01:53 +00:00 (Migrated from gitlab.com)

marked as related to #28

marked as related to #28
PlasticDigits commented 2026-08-08 09:58:25 +00:00 (Migrated from gitlab.com)

mentioned in commit 695e577065

mentioned in commit 695e57706584abbdd69ad0fe01c934ddcfe8092c
PlasticDigits commented 2026-08-08 09:58:36 +00:00 (Migrated from gitlab.com)

mentioned in merge request !45

mentioned in merge request !45
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/ust1-window#22
No description provided.