---
title: "Guardian Routines"
description: "Operator runbook for the guardian key: what to check before and after a lever lands, and when to escalate to the owner."
audience: tech
type: guide
status: live
lang: en
updated: "2026-09-17"
publish: true
---
# Guardian routines

The procedure side of the guardian key: the reads that say whether a call will work and whether it
worked, and the point at which the job is no longer yours. The levers themselves, their arguments and
their authority are [Guardian](/docs/3-1-4-guardian).

---

## 1. Before you act

- **Chain.** Confirm the wallet's chain before signing. Pool salts include `block.chainid`, so the
  same nominal address is a different pool elsewhere.
- **Role.** `AccessControl.isGuardian(you)` on the `AC` the *target* reads: `Admin.AC()`,
  `ExternalOracle.AC()`, `PoolFactory` likewise. `quorumStatus()` reports whether the policy is armed
  and whether the live multisig configuration still satisfies it.
- **That something is actually pending**, before any cancel. `pendingSigner` /
  `pendingSignerGrantOp`, `pendingSignerThresholdOp`, `pendingFeedWiden(feedId)` (0 = none),
  `AccessControl.pendingRole(role)`, `UpgradeGate.pendingUpgrade`. `Admin.pendingOps` is **internal**:
  there is no on-chain read for it, so the queue's ETA comes from the `TimelockRequested` event or
  the indexer. A cancel with nothing pending reverts `NoPending` and costs only gas.
- **The legs, before a halt.** Discovery is `PoolFactory.getOfficialPoolsForToken(token)` →
  `getPoolTokens(pool)` ([Access control](/docs/3-1-overview)).
  `batchRiskOp` takes the two arrays positionally; a length mismatch reverts `InvalidInput`.
- **Both current values, before a tighten.** `getFeed(feedId).maxDeviationBps` and `.ttlSecs`:
  `updateFeed` reverts on anything above either, and that revert is easy to misread as a role
  problem. Check `pendingFeedWiden(feedId)` in the same read: a tighten on a feed with a request
  queued voids that request, which may be the point or may be an accident.

---

## 2. After you act

Read state, not the transaction. Halt and pause events are snapshot triggers rather than indexed
topics, so the indexer will not answer "what is halted right now"
([Observability](/docs/3-3-observability)).

| You called | Verify | Event |
|---|---|---|
| `haltAsset` / `batchRiskOp` | `Pool.getAsset(token).flags` carries your bit | `AssetHalted` / `BatchRiskOp`, `BatchLegSkipped` |
| `collapseAnchor` | `getAsset(token).anchor` is the new anchor and the guardian bit is set | `AnchorUpdated` |
| `pauseFeed` | `getFeed(feedId)` flags bit 0 set; `isFeedFresh(feedId)` false. Off the Arc V4 pair the mark also reads 0 (DEAD), which is the lever working, not a failed write | `FeedPaused` |
| `updateFeed` | `getFeed(feedId).maxDeviationBps` and `.ttlSecs` | `FeedUpdated` |
| `revokeSession` | `session()` returns a zero relay | `SessionRevoked` |
| `setFeedExpBias` | `getFeed(feedId)` reads stale; `expHeadroom(feedId)` is `(0, 0)` until the next push | `FeedExpBiasUpdated` |
| `revokeSigner` | `signers(addr)` false; `signerCount` vs `signerThreshold` | `SignerRevoked` |
| `cancelTimelock` / `cancel*` | The pending read from [§1](#1-before-you-act) is now zero | `TimelockCancelled`, `SignerGrantCancelled`, `SignerThresholdDecreaseCancelled`, `FeedWidenCancelled`, `UpgradeCancelled`, `RoleCancelled` |
| `pause()` | `paused()` true on that singleton | `Paused(true)` |

Then check the blast radius you did not intend. After a `revokeSigner` sweep, `signerCount` below
`signerThreshold` means **no feed on that oracle can be pushed** and every one of them will age into
its TTL: a correct fail-safe, and also a deadline. After a halt, the fraction of the book still
tradable is `feeds.tradableRatio` and `bot_tradable_ratio`
([Observability](/docs/3-3-observability)).

---

## 3. Escalate

You cannot reverse anything you just did, so the owner is the second half of the action, not an
optional follow-up. Hand over as soon as the lever has landed:

- What you halted or paused, with the `src` bits and feed ids, so the release clears exactly those.
- Why, with the read that triggered it.
- What is now blocked that the incident did not require blocking.

Intake and contact are [Access control](/docs/3-1-overview)
and `security@btr.markets`. A suspected vulnerability rather than an operational fault goes through
[Audits](/docs/3-4-overview).

Escalate rather than act when the remedy is a widen, an un-halt, a grant, a param write or a value
movement. None of those are reachable from this key.

A feed wedged outside its band is the clearest case: the remedy is the owner's `requestFeedWiden` →
`LISTING` delay → `executeFeedWiden` (on the Arc V4 pair, which predates it, the V5 implementation
upgrade through `OracleBeacon`, `LISTING` tier, 1 day; there is no per-leg repoint). Hand it over
with the feed ids **grouped by slot**: the execute stamps one clock per slot and every wedged lane of
a stalled slot has to be released in the same batch, since a lane left out is fail-open on its own
leg.

`pauseFeed` is the instant lever for any lane that batch will not cover, and it is yours
([Oracle keeper](/docs/3-1-5-oracle-keeper)). Attempting any of the rest from here burns
gas and time during an incident.

**Releasing a feed pause is the owner's four steps, in this order.**

1. Verify the mark is back in band before touching the chain: the first push after release is
   banded against the mark `pauseFeed` froze, over the whole gap since it was observed, so compare
   the current NX Rates mid with that anchor against `maxDeviationBps` plus the σ term; a mid
   outside it needs `requestFeedWiden` → `LISTING` delay → `executeFeedWiden` first, or the release
   wedges the lane it was meant to free.
2. `unpauseFeed(feedId)` from the owner Safe (`ExternalOracleV5`, `onlyAdmin`; the guardian key
   cannot).
3. Confirm `getFeed(feedId)` flags bit 0 clear, then `isFeedFresh(feedId)` true once the keeper's
   next push lands; the lane reads DEAD in between, and that window is the expected one push cycle,
   not a failed write.
4. Clear the page only on that fresh read, and record the feed ids released against the ones the
   halt named.

---

## 4. Checklist

**On grant, before you are needed**

- [ ] `isGuardian(you)` is true on every `AC` you are expected to cover, and you know which
      `Admin` / `PoolFactory` / `ExternalOracle` instances read each one.
- [ ] The signing address is the whitelisted address (post-arm: the Safe itself, not its signers),
      and its threshold is within `guardianQuorumMax`.
- [ ] `quorumStatus()` reads armed with `ownerOk` true and a non-zero guardian count; if not, raise it now
      rather than during an incident ([Access control](/docs/3-1-overview)).
- [ ] You can produce a feed id and an asset address for every leg you cover, from a source that is
      not the front-end.
- [ ] You have signed a no-op transaction from this key on this chain at least once.

**Before signing**

- [ ] Chain id matches the target deployment.
- [ ] The lever is in [Guardian §2](/docs/3-1-4-guardian#2-the-levers): halt, tighten or cancel. If
      it is not, it is an owner action.
- [ ] Arguments checked against
      [Guardian §3](/docs/3-1-4-guardian#3-arguments-that-decide-whether-the-call-does-what-you-meant):
      `src` bits, `BatchOp.HALT`, `subject` padding, and for
      `updateFeed` **both** a band and a TTL at or below the live ones.
- [ ] For a cancel: the pending read from [§1](#1-before-you-act) is non-zero.
- [ ] For a halt: the leg list is enumerated, and the two arrays are the same length.

**After the transaction lands**

- [ ] The [§2](#2-after-you-act) state read confirms the change, not just a successful receipt.
- [ ] `BatchLegSkipped` count is zero, or every skipped leg is accounted for and retried.
- [ ] `signerCount` versus `signerThreshold` checked if you revoked.
- [ ] `feeds.tradableRatio` reviewed for collateral damage.
- [ ] Owner notified with the exact arguments used, because only the owner can undo them.
