---
title: "Risk Steward"
description: "Operator runbook for the bounded risk key: the one entrypoint, the exact clamp rules, what fails closed, and what to verify before and after a retune."
audience: tech
type: guide
status: live
lang: en
updated: "2026-09-17"
publish: true
aliases: [3-11-risk-steward-operations]
---
# Risk Steward

For the holder of a risk-steward grant (`AccessControl.isRiskSteward(addr)`) and for whoever operates
the automation that signs as one. The role's place in the authority model:
[Access Control](/docs/3-1-overview). Why risk parameters carry no timelock: [§4](#4-risk-parameters-are-deliberately-not-timelocked).

---

## 1. The role is two functions

`Admin.setAssetParamsBounded(address pool, address token, uint128 minLiquidity, uint16 minFeePbps,
uint16 vegaBps)` and `Admin.raiseKappa(address pool, address token, uint16 kappaCovBps)`, both gated
by `_onlySteward`. That is the entire surface. Neither queues: the call either writes in the same
transaction or reverts. On a foreign GEN-1 pool both lanes belong to the pool's seat, and a protocol
steward has no standing there.

`setAssetParamsBounded` moves `minFeePbps` and [`vegaBps`](/docs/glossary#vega) inside the owner's
hard fences; a raise lands as is, a lowering is bounded cumulatively to `maxDeltaBps` below the value
at the start of the current 24 h window (`AdminParams.StewardWindow`). `raiseKappa` raises
`kappaCovBps` and nothing else, and voids any live `UPDATE_RISK` op for the leg.

The grant is timelocked and contract-only: `AccessControl.queueRole(RISK_STEWARD, s)` runs
`_validateAddr`, which reverts `NotCode` for an address with no code, and `executeRole` lands it
after the LISTING delay. Revocation (`revokeRiskSteward`) is instant and never policy-checked, by the
rule guardians follow: a steward that turns out to be wrong must stay instantly removable.

| The steward can | The steward cannot |
|---|---|
| Raise or lower `minFeePbps` and `vegaBps` inside the owner's fences | Move `minLiquidity` at all ([§2](#2-the-clamp-rules-exactly)) |
| Raise `kappaCovBps` (never cut it) | List an asset, install a curve, upgrade the oracle, or change fees for the pool |
| Act with no delay, at the tempo the market moves | Halt, un-halt, pause, cancel anything, queue or execute a timelocked op, or move value |

Everything in the right-hand column is an owner action, except halting, which is a guardian action
([Guardian](/docs/3-1-4-guardian)). The steward exists so the fast half of risk
management does not require the owner key, and so reaching for it cannot become a back-door to the
rest.

---

## 2. The clamp rules, exactly

`setAssetParamsBounded` (`AdminParams.setBounded`) applies five checks in this order. The order is
the difference between a plan that lands and one that reverts opaquely mid-incident.

**1. Fences must be armed.** `RiskFences` are per `(pool, token)` and set by the owner via
`setRiskFences`. If `maxDeltaBps == 0` the call reverts `NotConfigured(ASSET, token)`. The lane fails
closed until the owner arms it: an unfenced asset is unreachable, not a free-for-all.

**2. `minLiquidity` must pass through unchanged.** If the value differs from the live
`Asset.minLiquidity`, the call reverts `InvalidInput`. Read the live asset and echo the field back.

**3. No lowering under a live owner op.** If the owner has an `UPDATE_ASSET_PARAMS` op queued for
this leg and that op moves `minFeePbps` (or `vegaBps`), a steward lowering of that field reverts
`AlreadyPending` until the op executes or is cancelled. A raise still lands, and the owner's stale
op then fails its own compare-and-swap.

**4. Hard fences, absolute and always applied.**

| Field | Bound | Revert |
|---|---|---|
| `minFeePbps` | `[minFeeHardMinPbps, minFeeHardMaxPbps]` | `ThresholdViolation` naming the breached side |
| `vegaBps` | `[vegaHardMinBps, vegaHardMaxBps]` | `ThresholdViolation` |

**5. The 24 h lowering envelope (`_loosenOk`).** A raise, or a value equal to the live one, passes.
A lowering must satisfy

```
new · 10000 >= anchor · (10000 - maxDeltaBps)
```

where `anchor` is the field's value when the current window opened (`Admin.stewardWindow`,
`AdminParams.StewardWindow`), ratcheted up by any raise on any lane inside the window. A window opens
lazily on the first call at or past the previous `end` and lasts `STEWARD_WINDOW = 24 hours`. The
bound is on cumulative *displacement* from the anchor, not on the step: N calls in a block compose to
one bounded move, and a raise followed by a lowering measures from the raised value.

On success the call emits `BoundedAssetParamsUpdated(pool, token, minFeePbps, vegaBps, tighten)`,
with `tighten = minFeePbps >= cur.minFeePbps && vegaBps >= cur.vegaBps`.

`raiseKappa` has one rule: `kappaCovBps > cur.kappaCovBps`, else `InvalidInput`; the pool's own
bounds still apply (`[50, 10000]`, and a spoke may not exceed its hub). It voids any live
`UPDATE_RISK` op for the leg, because that payload was an absolute `RiskConfig` authored against the
pre-raise κ and its execute would undo the raise; the owner re-queues. It emits
`RiskConfigUpdated(pool, token, flags)`.

---

## 3. What the owner's fences mean for you

You consume fences; you never write them. Their shape constrains what plans are expressible:

- `maxDeltaBps` is in `(0, 10000]`. It is the 24 h lowering budget for both fenced fields.
- `minFeeHardMinPbps` is required non-zero. A zero floor is not a floor; it would leave the ratchet
  open while looking fenced.
- Re-arming a fence (`setRiskFences`) deletes the leg's steward window, so the envelope re-opens from
  the state it was armed on.
- The owner's own unbounded `setAssetParams` is still floored by an armed `minFeeHardMinPbps`
  (`_requireFeeFloor`), re-applied at `executeSetAssetParams`. Lowering below an armed fence is
  deliberately two transactions, owner or not.
- That fee floor is the only fence that binds the owner lane. `vegaHardMinBps` / `vegaHardMaxBps` and
  `maxDeltaBps` are read by `setAssetParamsBounded` alone: the owner path writes any vega the global
  `PoolConfig` bounds admit, immediately when the write is a defensive tighten and through the TUNING
  queue otherwise. The owner is kept out of the fenced lane itself (`NotOwner`): a key that can widen
  the fences and write inside them in one transaction would bypass that queue.

If a plan you need is not expressible inside the live fences, the answer is a fence change by the
owner, not a sequence of steward calls that walks there.

---

## 4. Risk parameters are deliberately not timelocked

`setRiskFences`, `setAssetParamsBounded`, `raiseKappa` and `setRiskConfigTighten` bypass the timelock entirely. The steward lane writes exactly two fields (`minFeePbps`, `vegaBps`) and never queues; `minLiquidity` is an argument only so the call can prove it is unchanged, and any delta reverts `InvalidInput`. The matching `RiskFences` are `minFeeHardMinPbps`, `minFeeHardMaxPbps`, `vegaHardMinBps`, `vegaHardMaxBps` and `maxDeltaBps`: a hard min/max pair for each field, plus one cumulative lowering bound over a 24 h window.

A design boundary, not an oversight: BTR's edge is fast risk-parameter adaptivity, so those calls stay immediate and the control is the bounded envelope plus a dedicated, revocable risk role, not a delay. De-risking is free in every lane. Structural changes (asset add/remove, curve install) route through `requestOp` at `LISTING`; bounded numbers in the risk-up direction (`UPDATE_RISK`, `UPDATE_PROFILE`, `UPDATE_FEES`, a weakening `setAssetParams`) at `TUNING`.

---

## 5. What this key is the last line of defence against

The steward lane is what makes an untimelocked risk parameter safe: a key that can move the fast
scalars within the hour, but only inside bounds the owner set in advance and only in steps small
enough to be observed and reversed. The exposures it closes:

| Exposure | Lever | Bound |
|---|---|---|
| Adverse selection outrunning the deployed fee floor | Raise `minFeePbps` | Instant; inside `minFeeHardMaxPbps` |
| Realized volatility outrunning the quoted band | Raise `vegaBps`, the sensitivity knob of the adaptive dispersion law | Instant; inside `vegaHardMaxBps` |
| A drain the wall under-prices | Raise `kappaCovBps` via `raiseKappa` | Instant; `<= 10000`, and a spoke never above its hub |
| A fee floor left punitively high after a regime passes | Lower `minFeePbps` | At most `maxDeltaBps` below the 24 h window anchor; floored by `minFeeHardMinPbps` |

σ is not a steward lever and does not need to be: it adapts at push cadence through the signed oracle
blob with zero governance latency ([Oracle Keeper](/docs/3-1-5-oracle-keeper)). Sub-hour
band adaptation therefore needs only vega to track its target.

Shape is not a steward lever either. Splines are not hot-updatable, so a shape change repoints at a
pre-certified preset through the owner's `requestOp(UPDATE_PROFILE)` queue at the `TUNING` tier. The
dispersion band's ceiling is not a lever at all; it is the protocol constant
`PoolConstantsLib.MAX_DISPERSION_PBPS`.

---

## 6. The standing invariants

The steward lane fixes a breach; it does not detect one. Detection is the risk keeper's guardian
predicates, which observe and alert only: that module holds no executor and builds no plan, so there
is no path from a breach to a transaction. The predicate set and its severities are
[Observability](/docs/3-3-observability). The re-page cooldown is six
hours per predicate and key, chosen so a standing breach stays visible without training the operator
to filter the channel.

The one that couples this role to the oracle keeper:

> **`minFeePbps >= 2θ`** per supervised pool × asset, where θ is what the push keeper is *actually*
> running for that feed, not what the config you are editing says.

A fee floor below twice the push threshold is an anti-pick-off failure: the mark can move by θ
between pushes and a round trip inside `2θ` is free. The oracle keeper checks the same invariant over
its own `pools` list, which is empty in some shipped configs and therefore silent
([Oracle Keeper](/docs/3-1-5-oracle-keeper)); the risk keeper
checks it over every supervised pool because it already holds both halves of the comparison.

θ and the curve parameters are one optimisation variable. A θ change ships as an atomic bundle (the
fast scalars, the preset repoint, and an edit to the push keeper's `oracle.*.toml`), never as three
independent deploys. A partial deploy silently breaks the between-push discipline.

---

## 7. If the automation signs for you

`btr-keeper risk` is the supervised implementation of this role. What it enforces applies to a manual
retune too.

- **Double gate.** Live requires both `--execute` and `RISK_EXECUTE=1`, and signs with a dedicated
  steward key, never a money-path key.
- **Refuses the stronger key.** Startup reads `AccessControl.owner()` and aborts if it equals the
  configured signer. The fenced lane rejects the owner outright (`_onlySteward` reverts `NotOwner`),
  and a keeper holding that key is a far larger blast radius than the bounded lane.
- **Refuses to arm without the role.** `AccessControl.isRiskSteward(signer)` is read at startup: two
  `eth_call`s, never per sweep. Fatal when armed; in dry-run it reports and disables transaction
  simulation, so a `NotAuth` revert is not the only thing a dry run ever shows.
- **Global circuit breaker.** `max_updates_per_h` caps broadcast param updates per rolling hour
  across every pool and asset, and must land in `[1, 30]`: an armed keeper rejects `0` and anything
  above the `MAX_UPDATES_PER_H_CEILING` of 30. Per-asset limits alone are not a cap: every asset can
  satisfy its own simultaneously.
- **Refuses stale inputs.** `max_fit_age_h` bounds the artifact half, in `[1, 8760]` hours, and
  `max_measure_age_h` the live tape half, in `[1, 168]`. A class-default fallback row, or one whose
  `tapeStatus` is not `ok`, is refused outright unless `allow_provisional` is set; with it set the
  row may only tighten, and a loosen off a provisional row holds as `ProvisionalLoosen`.
- **Asymmetric deadbands and dwell.** A tighten crosses a smaller deadband than a loosen, and a
  tighten bypasses dwell entirely: only a loosen has to persist. The lever decides which floor it
  persists against, not the word "band": a θ move or a `minDispersionPbps` (scale) move takes
  `dwell_shape_s`, floored at 24 h, because both re-base the whole quote; the 1 h `dwell_band_s`
  floor applies only to the fee/vega FAST lane. `cooldown_s` is required to be at least
  `dwell_band_s`, so the emission tempo can never outrun the fastest dwell, and a successful emission
  resets the dwell clock rather than carrying the previous window forward.
- **An explicit hold list.** Assets under review are named in config, because a hold that lives only
  in a shell comment is not a control.
- **A pre-submit mirror of the chain.** The clamp rules of [§2](#2-the-clamp-rules-exactly) are re-implemented off chain and a plan
  that would revert is never built, so failures read as a named invariant rather than an opaque
  `ThresholdViolation`.

None of those numbers are protocol constants and none are on chain. Deadbands, dwell, caps, θ floors
and per-asset vega targets are deployment-specific and live in the risk keeper's `risk.<chain>.toml`,
bounded by the keeper-side floors and ceilings named above, which no config may go under or over. The
fences that bound the writes themselves live on chain in `Admin.riskFences(pool, token)`.

---

## 8. Before a write

- Read the live asset, `IPool(pool).getAsset(token)`: you need `minLiquidity` verbatim and the live
  `minFeePbps` / `vegaBps`.
- Read `Admin.riskFences(pool, token)`. `maxDeltaBps == 0` means the lane is shut; stop and ask the
  owner to arm it rather than retrying.
- For any lowering, reconstruct the window anchor: the field's value at the last window open, raised
  by any raise since. `end` is internal; derive it from your own `BoundedAssetParamsUpdated` history
  or assume the worst case, that the window is still the one your earlier lowering opened.
- Check for a live owner `UPDATE_ASSET_PARAMS` op on the leg (`TimelockRequested` without a matching
  execute or cancel). A lowering of a field it moves reverts `AlreadyPending`.
- Check the destination against the hard fences before the envelope, because a lowering inside
  `maxDeltaBps` can still land under `hardMin`.
- Check `minFeePbps >= 2θ` against the θ the push keeper is running now ([§6](#6-the-standing-invariants)).
- Simulate. The lane never queues, so there is no window in which to notice a mistake.

---

## 9. After a write

- `getAsset(token)` reflects the new `minFeePbps` and `vegaBps`, and `minLiquidity` is untouched.
- `BoundedAssetParamsUpdated` is in the receipt, and its `tighten` flag matches your classification.
  A mismatch means your model of the live state was stale.
- Realized fee is at or above the new floor: `feeAvgBps` against `minFeePbps / 100`
  ([Observability](/docs/3-3-observability)). There is no maximum; below
  the floor means mispriced risk.
- The window floor is `anchor · (1 - maxDeltaBps/10000)`; a later lowering in the same window
  measures against that same anchor, and a raise moves the anchor up, never the floor down.
- The guardian predicates that motivated the change have cleared, and no new one has opened.

Parameter and halt events are snapshot triggers rather than indexed topics, so retuning is not
queryable from the indexer ([Observability](/docs/3-3-observability)).
Verify against chain state.

---

## 10. Escalate

| Need | Owner or guardian |
|---|---|
| Fences too tight for the required plan, or unarmed (`NotConfigured`) | Owner: `setRiskFences` |
| A `minLiquidity` change | Owner: `setAssetParams`; queues unless it is a defensive tighten |
| A κ cut, a deposit-cap raise, un-gating deposits, a weight-cap change | Owner: `requestOp(UPDATE_RISK)` at the `TUNING` tier; a tighten lands now via `setRiskConfigTighten` |
| A preset repoint or dispersion floor change | Owner: `requestOp(UPDATE_PROFILE)` at the `TUNING` tier |
| Fee floor must go below an armed `minFeeHardMinPbps` | Owner: two transactions, `setRiskFences` first, by design |
| The leg must stop trading now | Guardian: `haltAsset` ([Guardian](/docs/3-1-4-guardian)) |
| A feed is the problem, not the parameters | Guardian: `pauseFeed`, or `updateFeed(feedId, maxDeviationBps, ttlSecs)` on `ExternalOracleV4` to tighten the band and the TTL. Both fields ratchet down from every instant lever, on the owner lane too; loosening either is the owner's timelocked `requestFeedWiden` → `executeFeedWiden` ([Oracle Keeper](/docs/3-1-5-oracle-keeper)) |

Contact and intake: `security@btr.markets`,
[Access Control §2.1](/docs/3-1-overview#21-escalation).

---

## 11. Checklist

**On grant**

- [ ] `isRiskSteward(you)` is true on the `AC` that `Admin.AC()` returns, and the granted address is
      the address that will sign.
- [ ] The signing key is not the owner key and not a money-path key.
- [ ] `riskFences(pool, token)` is armed for every asset you are expected to cover: `maxDeltaBps`,
      both fee bounds, both vega bounds.
- [ ] You can read live `getAsset` for every covered leg without the front-end.
- [ ] You know the θ each covered feed is running, and where that number is configured.

**Before each write**

- [ ] Live `Asset` read this block; `minLiquidity` echoed verbatim.
- [ ] `maxDeltaBps != 0`.
- [ ] Every lowering fits the 24 h envelope from the window anchor, not from the live value.
- [ ] No live owner `UPDATE_ASSET_PARAMS` op moves a field you are lowering.
- [ ] Destination inside every hard fence.
- [ ] `minFeePbps >= 2θ` still holds after the change.
- [ ] Simulated against current state, not against the plan that generated it.

**After each write**

- [ ] `getAsset` matches the intended values; `minLiquidity` unchanged.
- [ ] `BoundedAssetParamsUpdated.tighten` matches your classification.
- [ ] Realized `feeAvgBps` at or above the new floor once flow resumes.
- [ ] The rolling-hour update budget still has headroom for a reversal.
- [ ] The originating predicate has cleared and is not re-paging.

---

## 12. Related

| Page | Content |
|---|---|
| [Access Control](/docs/3-1-overview) | Where this role sits, and the owner lane beside it |
| [Admin](/docs/3-1-2-admin) | The owner's unbounded param lane |
| [Guardian](/docs/3-1-4-guardian) | The halt lever this role escalates to |
| [Oracle Keeper](/docs/3-1-5-oracle-keeper) | The θ half of the `minFee >= 2θ` invariant, and σ adaptation |
| [Observability](/docs/3-3-observability) | The predicates and the fee metrics named above |
