Risk Steward
For the holder of a risk-steward grant (bit 18 of AccessControl.perms(addr), PERM_RISK_STEWARD) and for whoever operates
the automation that signs as one. The role’s place in the authority model:
Access Control. Why risk parameters carry no timelock: §4.
1. The role is five functions
All gated by _onlySteward, none of them queuing: each call either writes in the same transaction or
reverts. On a foreign GEN-1 pool every lane belongs to the pool’s seat, and a protocol steward has no
standing there.
Admin.setAssetParamsBounded(address pool, address token, uint128 minLiquidity, uint16 minFeePbps, uint16 vegaBps, uint16 coopDiscBps, uint16 coopRebateBps) is the bounded lane: it moves
minFeePbps, vegaBps and the leg’s coop terms
(Pool §2.6) inside the owner’s hard fences, a tighten as is, a
loosen (fee, vega or rebate down, disc up) bounded cumulatively to maxDeltaBps off the value at
the start of the current 24 h window (AdminParamsLib.StewardWindow).
The other four are the one-way lane, bounded by direction instead of by size (Parametrization §2):
raiseMinDispersion(address pool, address token, uint32 minDispersionPbps)raises the quiet-tape dispersion floor at the leg’s livecurveId. The shape is the contract’s argument, not the caller’s, so the lane cannot repoint a curve.setRiskConfigTighten(address pool, address token, IPool.RiskConfig calldata cfg)writes a wholeRiskConfigonly if every field tightens against live state read in the same call. Tightens are switching a leg’s swap, liability-swap, flash or coop fill off (each on its own), arming the deposit or swap gate, cutting the deposit cap or the liability weight, and raising κ (liability-swap off also refuses a cross-assetwithdrawTointo or out of the leg; a same-asset exit stays open).narrowRefBand(address pool, address token, uint16 refBandBps)narrows the leg’s depeg band, not belowMIN_REF_BAND_BPS(50). Widening it is the owner’sUPDATE_ANCHOR, at GOVERNANCE.clearAssetHook(address pool, address token)recalls the leg’s whole yield-hook tranche and drops the hook, in one call. What a stressed venue cannot return yet stays out with the hook attached recall-only, and the next clear finishes.
A queued op is absolute, so one authored before the tighten would restore the old value when it executes. Each instant write therefore voids a queued op for the same leg only when that op would undo a field the write tightens, and the write lands either way. Per-lane void rules and tests: Admin §5.3.
The grant is timelocked and contract-only: AccessControl.queueRole(RISK_STEWARD, s) runs
_validateAddr, which reverts NotContract for an address with no code, and executeRole lands it
after the LISTING delay. Revocation (clearing the steward bit with setPerms) is instant and never policy-checked, the
same rule as for guardians: a compromised or wrong steward 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) |
Raise kappaCovBps, raise the dispersion floor, tighten a whole RiskConfig incl. switching swap / liability-swap / flash off, narrow the depeg band, remove a yield hook, halt a mark-store lane (never the reverse) | List an asset, install a curve or a hook, repoint a leg’s curve, raise the liquidity floor, upgrade the oracle, or change fees for the pool |
| Act with no delay | Halt an asset, un-halt anything, pause, cancel anything, queue or execute a timelocked op, or move value |
Everything in the right-hand column is an owner action, except halting an asset, which is a guardian action (Guardian). The steward exists so that fast risk management does not require the owner key, and so that the fast lane cannot become a back door to the rest.
2. The clamp rules, exactly
setAssetParamsBounded (AdminParamsLib.setBounded) applies five checks in this order. The order
determines which revert a bad plan hits.
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.
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, one-sided. A fence binds only a move toward it, so a live value the owner set outside one (a raise past a cap, a cut below a floor) never freezes the leg’s other fields.
| Field | Bound | Revert |
|---|---|---|
minFeePbps | never below minFeeHardMinPbps; a raise not above minFeeHardMaxPbps | ThresholdViolation naming the breached side |
vegaBps | a cut not below vegaHardMinBps; a raise not above vegaHardMaxBps | ThresholdViolation |
coopDiscBps | a raise not above maxCoopDiscBps | ThresholdViolation |
coopRebateBps | a cut not below minCoopRebateBps; a raise not above maxCoopRebateBps | 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,
AdminParamsLib.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.
A κ raise through setRiskConfigTighten keeps the pool’s own bounds ([50, 10000], and a spoke
may not exceed its hub). It emits RiskConfigUpdated(pool, token, flags); the void rules are in §1.
3. What the owner’s fences mean for you
You consume fences; you never write them. Their shape constrains what plans are expressible:
maxDeltaBpsis in(0, 10000]. It is the 24 h lowering budget for both fenced fields.minFeeHardMinPbpsis required non-zero. A zero floor would leave the ratchet open while appearing fenced.- Re-arming a fee, vega or
maxDeltaBpsfence (setRiskFences) deletes the leg’s steward window, so the envelope re-opens from the state it was armed on; a change to the coop bounds alone keeps it. - The owner’s own unbounded
requestAssetParamsis still floored by an armedminFeeHardMinPbps(_requireFences), re-applied atexecuteUpdateAssetParams. Lowering below an armed fence is two transactions, owner or not. - That fee floor is the only fence that binds the owner lane.
vegaHardMinBps/vegaHardMaxBpsandmaxDeltaBpsare read bysetAssetParamsBoundedalone: the owner path writes any vega the globalPoolConfigLibbounds 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, raiseMinDispersion,
setRiskConfigTighten, narrowRefBand and clearAssetHook bypass the timelock entirely, and two
different bounds make that safe.
The bounded lane writes four fields (minFeePbps, vegaBps and the two coop terms) inside the owner’s envelope;
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, maxDeltaBps, maxCoopDiscBps, minCoopRebateBps and
maxCoopRebateBps: a hard min/max pair for each field, plus one cumulative lowering bound over a 24 h window.
The one-way lane carries no size bound at all, because sign is the bound: a risk-reducing write executes instantly at any size, while the same field in the loosening direction keeps its tier. Two moves stay timelocked whichever way they point, neither being a one-dimensional comparison: a curve repoint changes shape, and a liquidity floor raise moves an owner-only field.
This is intended: fast risk-parameter adaptivity is the product’s edge, so those calls stay
immediate. The control is direction plus a dedicated, revocable risk role, not a delay. The
worst a compromised steward reaches is denial of service, a pool tightened until it stops quoting;
recovery is clearing the steward bit (setPerms(s, PERM_RISK_STEWARD, false)), itself untimelocked, then the normal owner path.
That DoS reach is accepted, not bounded per call: a key that loops is not stopped by a step size.
One raiseMinDispersion can take a leaf’s floor to MAX_DISPERSION_PBPS, and one narrowRefBand
can take a volatile spoke’s band to the 50 bps stable floor, where ordinary tape trips it; the
steward’s instant mark-store lane halt has the same shape, and only the owner’s unhalt lifts it. Runbook: clear the
steward or guardian bit (setPerms(addr, mask, false)) at once, then restore through the owner
queue: the floor via UPDATE_PROFILE (TUNING), the band via UPDATE_ANCHOR (GOVERNANCE, so that
leg stays narrow for the delay), a lane halt via unhalt. 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 requestAssetParams)
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 setRiskConfigTighten | Instant; <= 10000, and a spoke never above its hub |
| A quiet-tape band too narrow to survive a push interval | Raise minDispersionPbps via raiseMinDispersion | Instant; at the live curveId, under that curve’s dispersionCap, and on an interior-capable leg under it at the live σ |
| A fee floor left high after a regime passes | Lower minFeePbps | At most maxDeltaBps below the 24 h window anchor; floored by minFeeHardMinPbps |
σ is not a steward lever: it adapts at push cadence through the signed oracle blob with zero governance latency (Mark Relayer). 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 curve through the owner’s requestOp(UPDATE_PROFILE) queue at the TUNING tier. The
band’s floor under that shape is a steward lever in one direction, raiseMinDispersion (§1); its
ceiling is the protocol constant PoolConstantsLib.MAX_DISPERSION_PBPS and not a lever.
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. Re-pages are cooled down per breach and key, so a standing breach stays visible without flooding 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. On aQUOTE_UNIT_UOAleg the mark divides out the base’s USD price, so the floor is2(θ_leg + θ_base), θ_base being the base reference lane’s (Parametrization §11).
The fee floor is held at a multiple of the push threshold, so no round trip inside one push interval is free. Both keepers enforce it: the oracle keeper over the pools it supervises (Mark Relayer), the risk keeper 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 curve repoint, and the matching push-keeper edit), never as three independent deploys. A partial deploy 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 broadcasting requires two independent arming flags; neither is set by default. It 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 (_onlyStewardrevertsNotOwner), and a keeper holding that key is a far larger blast radius than the bounded lane. - Refuses to arm without the role.
AccessControl.perms(signer)is read at startup and must carry the steward bit: twoeth_calls, never per sweep. Fatal when armed; in dry-run it reports and disables transaction simulation, so a dry run shows more than aNotAuthorizedrevert. - Global circuit breaker. Automated action is rate-limited across every pool and asset on a rolling hour, inside a bounded range an armed keeper refuses to leave; alerting is never bounded by that limit. Per-asset limits alone are not a cap: every asset can satisfy its own simultaneously.
- Refuses stale inputs.
max_fit_age_hbounds the artifact half, in[1, 8760]hours, andmax_measure_age_hthe live tape half, in[1, 168]. A class-default fallback row, or one whosetapeStatusis notok, is refused outright unlessallow_provisionalis set; with it set the row may only tighten, and a loosen off a provisional row holds asProvisionalLoosen. - 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 takesdwell_shape_s, floored at 24 h, because both re-base the whole quote; the 1 hdwell_band_sfloor applies only to the fee/vega FAST lane.cooldown_sis required to be at leastdwell_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 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 needminLiquidityverbatim and the liveminFeePbps/vegaBps. - Read
Admin.riskFences(pool, token).maxDeltaBps == 0means 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.
endis internal; derive it from your ownBoundedAssetParamsUpdatedhistory or assume the worst case, that the window is still the one your earlier lowering opened. - Check for a live owner
UPDATE_ASSET_PARAMSop on the leg (TimelockRequestedwithout a matching execute or cancel). THIS bounded fee/vega lane revertsAlreadyPendingagainst one, so clear it first. The instantRiskConfigand dispersion-floor lanes never revert that way: a queued op at least as tight as the write survives beside it. - Check the destination against the hard fences before the envelope, because a lowering inside
maxDeltaBpscan still land underhardMin. - Check
minFeePbps >= 2θ(2(θ_leg + θ_base)on a UOA leg) against the θ the push keeper is running now (§6). - Simulate. The lane never queues, so there is no window in which to notice a mistake.
9. After a write
getAsset(token)reflects the newminFeePbpsandvegaBps, andminLiquidityis untouched.BoundedAssetParamsUpdatedis in the receipt, and itstightenflag matches your classification. A mismatch means your model of the live state was stale.- Realized fee is at or above the new floor:
feeAvgBpsagainstminFeePbps / 100(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). 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: requestAssetParams; any post-seal change queues (a write before sealBootstrap lands at once) |
| A κ cut, a deposit-cap raise, un-gating deposits, a weight-cap change, switching swap / liability-swap / flash back on | Owner: requestOp(UPDATE_RISK) at the TUNING tier; a tighten lands now via setRiskConfigTighten |
| A wider depeg band | Owner: requestOp(UPDATE_ANCHOR) at the GOVERNANCE tier |
| A curve 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 |
| The leg must stop trading now | Guardian: haltAsset (Guardian) |
| A feed is the problem, not the parameters | Steward or guardian: mark-store halt(lane, tierMask), instant; only the owner unhalts. Lane bounds (ttl, maxDev, σ floor) are an immutable class table: a change is a new class table at a fleet upgrade (Mark Relayer) |
Contact and intake: security@btr.markets,
Access Control §2.1.
11. Checklist
On grant
perms(you) & PERM_RISK_STEWARDis non-zero on theACthatAdmin.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
getAssetfor 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
Assetread this block;minLiquidityechoed verbatim. maxDeltaBps != 0.- Every lowering fits the 24 h envelope from the window anchor, not from the live value.
- No live owner
UPDATE_ASSET_PARAMSop moves a field you are lowering. - Destination inside every hard fence.
minFeePbps >= 2θ(2(θ_leg + θ_base)on a UOA leg) still holds after the change.- Simulated against current state, not against the plan that generated it.
After each write
getAssetmatches the intended values;minLiquidityunchanged.BoundedAssetParamsUpdated.tightenmatches your classification.- Realized
feeAvgBpsat 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 | Where this role sits, and the owner lane beside it |
| Admin | The owner’s unbounded param lane |
| Guardian | The halt lever this role escalates to |
| Mark Relayer | The θ half of the minFee >= 2θ invariant, and σ adaptation |
| Observability | The predicates and the fee metrics named above |