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.


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

You calledVerifyEvent
haltAsset / batchRiskOpPool.getAsset(token).flags carries your bitAssetHalted / BatchRiskOp, BatchLegSkipped
collapseAnchorgetAsset(token).anchor is the new anchor and the guardian bit is setAnchorUpdated
pauseFeedgetFeed(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 writeFeedPaused
updateFeedgetFeed(feedId).maxDeviationBps and .ttlSecsFeedUpdated
revokeSessionsession() returns a zero relaySessionRevoked
setFeedExpBiasgetFeed(feedId) reads stale; expHeadroom(feedId) is (0, 0) until the next pushFeedExpBiasUpdated
revokeSignersigners(addr) false; signerCount vs signerThresholdSignerRevoked
cancelTimelock / cancel*The pending read from §1 is now zeroTimelockCancelled, SignerGrantCancelled, SignerThresholdDecreaseCancelled, FeedWidenCancelled, UpgradeCancelled, RoleCancelled
pause()paused() true on that singletonPaused(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).


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 and security@btr.markets. A suspected vulnerability rather than an operational fault goes through Audits.

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 requestFeedWidenLISTING 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). 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 requestFeedWidenLISTING 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).
  • 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: halt, tighten or cancel. If it is not, it is an owner action.
  • Arguments checked against Guardian §3: 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 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 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.