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 theACthe target reads:Admin.AC(),ExternalOracle.AC(),PoolFactorylikewise.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.pendingOpsis internal: there is no on-chain read for it, so the queue’s ETA comes from theTimelockRequestedevent or the indexer. A cancel with nothing pending revertsNoPendingand costs only gas. - The legs, before a halt. Discovery is
PoolFactory.getOfficialPoolsForToken(token)→getPoolTokens(pool)(Access control).batchRiskOptakes the two arrays positionally; a length mismatch revertsInvalidInput. - Both current values, before a tighten.
getFeed(feedId).maxDeviationBpsand.ttlSecs:updateFeedreverts on anything above either, and that revert is easy to misread as a role problem. CheckpendingFeedWiden(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 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 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).
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
srcbits 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 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). 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.
- Verify the mark is back in band before touching the chain: the first push after release is
banded against the mark
pauseFeedfroze, over the whole gap since it was observed, so compare the current NX Rates mid with that anchor againstmaxDeviationBpsplus the σ term; a mid outside it needsrequestFeedWiden→LISTINGdelay →executeFeedWidenfirst, or the release wedges the lane it was meant to free. unpauseFeed(feedId)from the owner Safe (ExternalOracleV5,onlyAdmin; the guardian key cannot).- Confirm
getFeed(feedId)flags bit 0 clear, thenisFeedFresh(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. - 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 everyACyou are expected to cover, and you know whichAdmin/PoolFactory/ExternalOracleinstances 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 withownerOktrue 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:
srcbits,BatchOp.HALT,subjectpadding, and forupdateFeedboth 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.
BatchLegSkippedcount is zero, or every skipped leg is accounted for and retried.signerCountversussignerThresholdchecked if you revoked.feeds.tradableRatioreviewed for collateral damage.- Owner notified with the exact arguments used, because only the owner can undo them.