---
title: "Guardian"
description: "The guardian key: what membership is, the levers it holds, the arguments that decide what a call does, and what it is the last line of defence against."
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-17"
publish: true
aliases: [3-9-guardian-operations]
---
# Guardian

For the holder of an address in `AccessControl.isGuardian`. Who may do what, and why the surface is
shaped this way, belongs to [Access control](/docs/3-1-overview). This page is the authority and the
semantics: the calls, their arguments, and the failures they exist to cover. The procedure around
them is [Guardian routines](/docs/3-2-2-guardian-routines).

---

## 1. What the key is

Membership is the **address itself**. `AccessControl.setGuardian(g, s)` whitelists one address: an
EOA before `armQuorumPolicy`, a Safe-compatible k-of-n after it. The signers of a guardian Safe are
not guardians; the Safe address is the entry. `guardianQuorumMax` defaults to
`QuorumLib.GUARDIAN_QUORUM_DEFAULT = 1` and is hard-capped at `GUARDIAN_QUORUM_CEILING = 2`, above
which the lever stops being fast.

Three properties define the remit, and every entrypoint below obeys all three:

1. **Direction.** Halt, tighten, cancel. Never un-halt, never widen, never grant.
2. **No value.** No guardian entrypoint moves a token or changes a recipient.
3. **No reverse.** No lever you pull is released by you. Most are released by the owner;
   `UpgradeGate.pause()` is the one that leaves a state to undo. `ExternalOracleV4.updateFeed` is
   released only by the owner's timelocked `requestFeedWiden` → `executeFeedWiden`, which you may
   veto but not call ([§2](#2-the-levers)).

One entrypoint, `setFeedExpBias`, sits outside properties 1 and 2 as a break-glass price write; its
warning is in [§2](#2-the-levers).

A wrong guardian call costs downtime; a late one costs the pool. Size your hesitation accordingly.

---

## 2. The levers

Each call runs `requireGuardianOrOwner(AC)` (or, on `UpgradeGate`,
`isGuardianOrAuth(msg.sender, authority)`). The `AC` a target reads is its own immutable: a guardian
on one deployment is nobody on another.

| Contract | Call | Effect | Released by |
|---|---|---|---|
| `Admin` | `haltAsset(address pool, address token, uint16 src)` | Sets halt bits on one leg | `unhaltAsset`, owner |
| `Admin` | `batchRiskOp(address[] pools, address[] tokens, BatchOp op, uint16 src)` | Same, across pairs, one tx | `batchRiskOp(UNHALT)`, owner |
| `Admin` | `collapseAnchor(address pool, address token, address newAnchor)` | Re-anchors one leg onto a strict ancestor and halts it. Unreachable on a depth-1 pool - see below | `unhaltAsset` + `UPDATE_ANCHOR` queue, owner |
| `Admin` | `cancelTimelock(address pool, uint8 opType, bytes32 subject)` | Vetoes any queued pool governance op | Owner re-requests |
| `Admin` / `Flash` | `pause()` | Freezes `executeUpgrade` on that singleton | `unpause`, owner |
| `Admin` / `Flash` | `cancelUpgrade()` | Discards the pending UUPS request | Owner re-requests |
| `PoolFactory` | `cancelReferenceUpgrade()` | Discards the pending beacon request | Owner re-requests |
| `ExternalOracleV4` | `pauseFeed(bytes32 feedId)` | Fail-closes one feed. Since the 2026-09-03 source (not on the Arc V4 pair) it also clears the lane and anchors the frozen mark, so the feed reads DEAD through the pause and across the release ([§4](#4-what-you-are-the-last-line-of-defence-against)) | `unpauseFeed`, owner |
| `ExternalOracleV4` | `updateFeed(bytes32 feedId, uint16 maxDeviationBps, uint16 ttlSecs)` | Tightens the per-push band and the TTL. Tighten-or-equal on both; nothing widens either back | Nothing. Owner cannot loosen it either |
| `ExternalOracleV4` | `revokeSession()` | Kills the live push session. Not a durable relay cutoff on V4: an open session IS in-band mark authorship (`pushV4` checks only `msg.sender == relay`, expiry and `seq <= maxSeq`, no signature), and the honest keeper re-grants within ~30 s. Permanent only after `revokeSigner` drops `signerCount < signerThreshold`, which makes every future `openSession` unsatisfiable | `openSession`, k-of-n quorum |
| `ExternalOracleV4` | `revokeSigner(address signer)` | Removes one attester key immediately | `requestSignerGrant` → `LISTING` delay, owner |
| `ExternalOracleV4` | `setFeedExpBias(bytes32 feedId, int8 newBias)` | Break-glass, and a price write. See the warning below | Another `setFeedExpBias`; no timelock either way |
| `ExternalOracleV4` | `cancelSignerGrant()` | Vetoes the pending signer addition | Owner re-requests |
| `ExternalOracleV4` | `cancelSignerThresholdDecrease()` | Vetoes the pending quorum decrease | Owner re-requests |
| `ExternalOracleV4` | `cancelFeedWiden(bytes32 feedId)` | Vetoes a pending band/ttl widen or wedge release, live or already expired | Owner re-requests |
| `AccessControl` | `cancelRole(Role role)` | Vetoes a queued `FACTORY` / `TREASURY` rotation | Owner re-queues |

**`updateFeed` is a ratchet, and its only inverse is an owner ceremony you can veto but not call.**
The owner's `updateFeed` is the same tighten-or-equal function as yours. A band or TTL tightened too
far, or a feed wedged past `10·maxDeviationBps`, is released by the owner's
`requestFeedWiden(feedId, maxDeviationBps, ttlSecs)` → `LISTING` delay → `executeFeedWiden(feedId)`.
`narrowMaxDeviation` does not exist on V4 in any release. Three consequences for this key:

- **Your tighten always wins.** The request snapshots the live band and ttl and the execute reverts
  `InvalidState` unless both still match, so an `updateFeed` you land during the delay voids the
  pending widen rather than being silently undone by it.
- **You hold the veto.** `cancelFeedWiden(feedId)` is guardian-or-owner and works on a live request
  and an expired one alike. The owner may re-request immediately, so a cancel buys review time, not
  an outcome.
- **You cannot release.** Handing a wedged feed back to quoting is an owner action, and on the Arc
  V4 pair, which predates the widen, the fallback is the V5 implementation upgrade through
  `OracleBeacon` (`LISTING` tier, 1 day); there is no per-leg repoint. Treat your own tighten as
  irreversible from this key.

> **`setFeedExpBias` sits outside the three properties above, and it is the one lever on this page
> that writes a price.** Decode is `mark = mant << (exp + bias)`, so a bias change moves the
> published mark by a power of two. It is `_onlyGuardianOrAdmin`, untimelocked, and kept as
> break-glass because the guardian already holds pause and halt. Blast radius, exactly: the target
> lane's price, σ and confidence are zeroed (the feed reads STALE until its next push), the whole
> slot's clock is stamped to now, so **the other seven lanes sharing that slot lose one push cycle**,
> and the new bias takes effect for every subsequent decode of that lane. The routine path is the
> quorum-signed `setFeedExpBiasSigned`, which carries the same k-of-n as a push
> ([Oracle keeper](/docs/3-1-5-oracle-keeper)). Whether the guardian arm
> should exist at all is an **open design question**: it is a price lever on a key whose remit is
> halt, tighten and cancel. Do not reach for it outside an incident where the alternative is a dark
> feed, and escalate to the owner the moment you do.

**`collapseAnchor` needs a tree at least two deep, so it cannot fire on the live fleet.** The walk
starts at the leg's current anchor and steps to *that* node's anchor before its first comparison, so
`newAnchor` must be a **strict** ancestor of the current anchor: a grandparent or higher. On the Arc
pools every spoke anchors directly to the base and the base anchors to nothing, so the first step
lands on `address(0)` and the call reverts `InvalidAnchor` for every leg, base included. It is a
topology repair for a depth ≥ 2 pool, not an incident lever. During an incident on this fleet the
leg-level lever is `haltAsset` / `batchRiskOp`; re-rooting a leg is the owner's `UPDATE_ANCHOR` op at
the `GOVERNANCE` tier.

`cancelRole(GUARDIAN_REVOKE)` is the one cancel the guardian cannot reach: owner-only by design, so
a guardian facing removal cannot veto its own revocation.

Every oracle row above is `_onlyGuardianOrAdmin`, except `revokeSession`, which any one signer may
also call. Which instance a deployment's pools actually read is
[Oracle keeper](/docs/3-1-5-oracle-keeper); pausing the idle one stops nothing. On Arc
the live pair is the V4 primary `0x842c2736…` and the V4 reference `0xC17920b2…`; V3 is the rollback
and holds no live leg.

---

## 3. Arguments that decide whether the call does what you meant

**`src` on `haltAsset` / `batchRiskOp`.** One or more bits of `PoolConstantsLib.HALT_MASK`:
`HALT_RISK_BIT` = bit 0 (`0x0001`), `HALT_GUARDIAN_BIT` = bit 6 (`0x0040`). Sources **refcount**:
`unhaltAsset(pool, token, src)` clears only the bits passed, so a leg stays halted while any other
source still holds it. Halt under your own bit. Halting under `HALT_RISK_BIT` is permitted (authority
is by edge, not by source), but it makes your action indistinguishable from an owner risk halt, and
the owner clearing theirs will then relist a leg you meant to hold.

**`op` on `batchRiskOp`.** `BatchOp.HALT` = `0`, `BatchOp.UNHALT` = `1`. A guardian passing `1`
reverts `NotAuth` before any leg is touched.

**Per-leg outcomes on `batchRiskOp`.** The loop is `try`/`catch` per leg, so one bad pair never
bricks the sweep, and a failed leg is **not halted**. The receipt distinguishes them: `BatchRiskOp`
per applied leg, `BatchLegSkipped` per skipped one. Count the events against the arrays you sent.

**`subject` on `cancelTimelock`.** The third key component. For token-keyed ops it is the asset
address left-padded (`bytes32(uint256(uint160(token)))`), and for `UPDATE_CURVE` it is the preset
id. `MIGRATE_BASE_TOKEN`, `UPDATE_TREASURY` and `UPDATE_FEES` are pool-level and ignore it. Cancel
shares `_keyOf` with `requestOp`, so every key a request can create is reachable and a wrong subject
reverts `NoPending` rather than cancelling something else. An unknown or `NONE` `opType` reverts
`InvalidInput`.

**Both arguments on `updateFeed`.** `maxDeviationBps` and `ttlSecs` are written together, and each
must be non-zero and **less than or equal to** the live value; anything else reverts `InvalidInput`.
There is no way to pass one and leave the other alone, so read both off `getFeed(feedId)` first and
pass the current TTL back verbatim unless you mean to shorten it. Passing the live values unchanged
is accepted and is a loud no-op that still emits `FeedUpdated`. A shortened TTL is as irreversible as
a narrowed band, and it fail-closes the leg sooner: at `ttlSecs` the feed reverts `StaleData` and the
leg stops trading.

**`revokeSigner`.** Silently a no-op if the address is not a granted signer. Revoking below
`signerThreshold` is unblocked by design: it halts pushing, the fail-safe response to a suspected key
compromise, not an error.

---

## 4. What you are the last line of defence against

The automated breakers cover what can be stated as a predicate: staleness TTL, the
`MAX_CONFIDENCE_HALT_BPS = 1000` ceiling, base parity, the per-asset reference band
([Flow guards](/docs/3-2-1-flow-guards), [Oracle keeper](/docs/3-1-5-oracle-keeper)).
The guardian exists for what they do not cover: a mark that is authentic, in band, fresh and wrong;
a queued governance op that should not land; a signer key believed leaked; an upgrade that should
not execute at its ETA.

| Situation | Lever | What it does not fix |
|---|---|---|
| One feed suspect, rest of the catalog healthy | `pauseFeed(feedId)` | The mark is not corrected, only withheld. Every consumer of that feed fail-closes, through the pause and until a push lands after it |
| Attester key believed leaked | `revokeSigner(addr)`, repeat per key | Pushing stops for every feed on that oracle once the set drops below `signerThreshold` |
| Relay or session key believed leaked | `revokeSession()`, then `revokeSigner(addr)` to drop the set below `signerThreshold`; owner rotates the NXR roster and the keeper Secret | `revokeSession` alone: the keeper re-grants the same address within ~30 s, so while `signerCount >= signerThreshold` the leaked relay authors marks bounded only by the per-lane deviation band |
| Band too wide for the observed tape | `updateFeed` (band down, TTL passed back unchanged) | Nothing already written; it bounds the next push only, and it cannot be widened back |
| Queued loosening you do not trust | The matching `cancel*` | The owner may re-request immediately; a cancel buys review time, not a veto on the outcome |
| A queued band widen you do not trust | `cancelFeedWiden(feedId)`, or an `updateFeed` tighten, which voids it by compare-and-swap | Neither reaches a widen already executed. A released feed reads DEAD until its next push, and that push is unbanded ([§2](#2-the-levers)) |
| Matured upgrade you want frozen, not killed | `pause()` on `Admin` / `Flash` | Only `executeUpgrade` is blocked. The request survives and remains executable after `unpause` |
| One leg's anchor chain suspect, on a pool at least two deep | `collapseAnchor(pool, token, newAnchor)` | `newAnchor` must be a strict ancestor of the leg's current anchor (`PoolConfig.collapseAnchor` walks up to `AnchorTreeLib.MAX_DEPTH`, else `InvalidAnchor`). On a depth-1 pool no such node exists and the call always reverts ([§2](#2-the-levers)). The write halts the leg in the same transaction |
| An asset must stop trading now | `haltAsset` / `batchRiskOp` | Halting a leg does not stop the same asset elsewhere; enumerate ([Access control](/docs/3-1-overview)) |

Three scope facts to internalise before an incident:

- **`Admin.haltAsset`** calls `IPool(pool).adminHaltAsset`, which is gated on the `Admin` singleton,
  not on the pool's creator: pool deployment is permissionless, but administration resolves to the
  same singletons, so the lever reaches **any** pool the fleet's `Admin` governs, official or not.
- **A paused feed** is **skipped**, not reverted: reverting on one paused feed would starve every
  other feed in the same blob. On V4 the same fail-soft covers an unregistered lane, a sentinel
  write, a band breach and an over-cap σ; a skipped lane is reported in
  `LanesSkipped(slotId, laneMask)`. Only framing, quorum, session and acceptance-window failures
  revert the whole push.
- **The pause is fail-closed on release too.** `pauseFeed` does not merely set the bit and leave the
  mark standing: it clears the lane's price and confidence and anchors the mark it froze together
  with the observation second the slot then held. The feed reads **DEAD** for the whole pause window
  and stays dead after `unpauseFeed` until a push lands, a consumer reading `getFeed` without `gate`
  sees the same thing, and that first push back is banded over the **real** gap since the frozen
  mark was observed, not over one cadence.
  - On the Arc V4 pair, deployed 2026-09-01 and not upgradeable, the slot clock still advances
    through a pause (a paused lane's entries still count as accepted, and its slot-mates keep
    pushing), so an unpaused feed reports its stale mark at age ~0 with a zero staleness premium,
    and the correcting push is banded so tightly that the release itself can wedge the feed.
  - Two operational consequences: **a pause is not a way to hold a mark**, and the leg stays dark
    for one push cycle after the unpause.
  - The slot clock is not stamped by design, so the pause costs the other seven lanes nothing
    ([Oracle keeper](/docs/3-1-5-oracle-keeper)).

A guardian key that can pause feeds is also what the feed-liveness keeper holds; its automated
`pauseFeed` path and the budget that bounds it are
[Oracle keeper](/docs/3-1-5-oracle-keeper).

---

## 5. Related

| Page | Content |
|---|---|
| [Guardian routines](/docs/3-2-2-guardian-routines) | Before, after, escalation and the checklist for every lever above |
| [Access control](/docs/3-1-overview) | Authority matrix, halt sources, timelock schedule |
| [Oracle keeper](/docs/3-1-5-oracle-keeper) | The push side of the same oracle, and the automated guardian |
| [Risk steward](/docs/3-1-3-risk-steward) | The other bounded key |
| [Flow guards](/docs/3-2-1-flow-guards) | The automated breakers that run without you |
| [Observability](/docs/3-3-observability) | The reads named above, and where they are served |
| [Admin](/docs/3-1-2-admin) | Upgrade procedure a `pause()` interrupts |
