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. 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.


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 requestFeedWidenexecuteFeedWiden, which you may veto but not call (§2).

One entrypoint, setFeedExpBias, sits outside properties 1 and 2 as a break-glass price write; its warning is in §2.

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.

ContractCallEffectReleased by
AdminhaltAsset(address pool, address token, uint16 src)Sets halt bits on one legunhaltAsset, owner
AdminbatchRiskOp(address[] pools, address[] tokens, BatchOp op, uint16 src)Same, across pairs, one txbatchRiskOp(UNHALT), owner
AdmincollapseAnchor(address pool, address token, address newAnchor)Re-anchors one leg onto a strict ancestor and halts it. Unreachable on a depth-1 pool - see belowunhaltAsset + UPDATE_ANCHOR queue, owner
AdmincancelTimelock(address pool, uint8 opType, bytes32 subject)Vetoes any queued pool governance opOwner re-requests
Admin / Flashpause()Freezes executeUpgrade on that singletonunpause, owner
Admin / FlashcancelUpgrade()Discards the pending UUPS requestOwner re-requests
PoolFactorycancelReferenceUpgrade()Discards the pending beacon requestOwner re-requests
ExternalOracleV4pauseFeed(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)unpauseFeed, owner
ExternalOracleV4updateFeed(bytes32 feedId, uint16 maxDeviationBps, uint16 ttlSecs)Tightens the per-push band and the TTL. Tighten-or-equal on both; nothing widens either backNothing. Owner cannot loosen it either
ExternalOracleV4revokeSession()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 unsatisfiableopenSession, k-of-n quorum
ExternalOracleV4revokeSigner(address signer)Removes one attester key immediatelyrequestSignerGrantLISTING delay, owner
ExternalOracleV4setFeedExpBias(bytes32 feedId, int8 newBias)Break-glass, and a price write. See the warning belowAnother setFeedExpBias; no timelock either way
ExternalOracleV4cancelSignerGrant()Vetoes the pending signer additionOwner re-requests
ExternalOracleV4cancelSignerThresholdDecrease()Vetoes the pending quorum decreaseOwner re-requests
ExternalOracleV4cancelFeedWiden(bytes32 feedId)Vetoes a pending band/ttl widen or wedge release, live or already expiredOwner re-requests
AccessControlcancelRole(Role role)Vetoes a queued FACTORY / TREASURY rotationOwner 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). 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; 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, 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.

SituationLeverWhat it does not fix
One feed suspect, rest of the catalog healthypauseFeed(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 leakedrevokeSigner(addr), repeat per keyPushing stops for every feed on that oracle once the set drops below signerThreshold
Relay or session key believed leakedrevokeSession(), then revokeSigner(addr) to drop the set below signerThreshold; owner rotates the NXR roster and the keeper SecretrevokeSession 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 tapeupdateFeed (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 trustThe 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 trustcancelFeedWiden(feedId), or an updateFeed tighten, which voids it by compare-and-swapNeither reaches a widen already executed. A released feed reads DEAD until its next push, and that push is unbanded (§2)
Matured upgrade you want frozen, not killedpause() on Admin / FlashOnly executeUpgrade is blocked. The request survives and remains executable after unpause
One leg’s anchor chain suspect, on a pool at least two deepcollapseAnchor(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). The write halts the leg in the same transaction
An asset must stop trading nowhaltAsset / batchRiskOpHalting a leg does not stop the same asset elsewhere; enumerate (Access control)

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).

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.


PageContent
Guardian routinesBefore, after, escalation and the checklist for every lever above
Access controlAuthority matrix, halt sources, timelock schedule
Oracle keeperThe push side of the same oracle, and the automated guardian
Risk stewardThe other bounded key
Flow guardsThe automated breakers that run without you
ObservabilityThe reads named above, and where they are served
AdminUpgrade procedure a pause() interrupts