Skip to content

feat: add CashRiskSteward for bounded, rate-limited Aave v4 drawCap tuning#228

Draft
seongyun-ko wants to merge 2 commits into
feat/lendfrom
feat/cash-risk-steward
Draft

feat: add CashRiskSteward for bounded, rate-limited Aave v4 drawCap tuning#228
seongyun-ko wants to merge 2 commits into
feat/lendfrom
feat/cash-risk-steward

Conversation

@seongyun-ko

@seongyun-ko seongyun-ko commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds src/aave-v4/CashRiskSteward.sol — a bounds-enforcing wrapper around Aave v4's HubConfigurator.updateSpokeDrawCap, so a low-trust keeper (EOA / 1-of-N) can tune the ether.fi Cash spoke's borrow cap to track live supply (keeping utilization stable and deposits withdrawable) while governance keeps the cap inside an on-chain band the keeper can never leave.
  • Closes the Aave v4 gap: the drawCap value is unbounded on-chain (Hub._updateSpokeConfig does a raw write with no ceiling/floor check, aave-v4 @ cdacec50 Hub.sol:710). A key granted that selector directly could set drawCap to 0 (freeze all Cash borrows) or MAX_ALLOWED_SPOKE_CAP (uncap → 100% utilization → rate spike + LP withdrawals block). Granting the AccessManager drawCap role to this contract instead of the keeper confines every keeper move to [floor, ceiling] throttled by a cooldown and a per-update step.

Protections

Governance sets the band; the keeper can only move within it, throttled:

  • (min, max) capminDrawCapFloor / maxDrawCapCeiling, enforced on every path. The bounds setter forces ceiling < MAX_ALLOWED_SPOKE_CAP, so the spoke can never be uncapped — even by governance.
  • Rate limit — a cooldown (min seconds between updates) plus maxStepBps, a percentage of the current cap (e.g. 500 = ±5% per cooldown). Expressing the step in BPS makes it scale with volume as the book grows (no governance retune) and matches Aave's RiskSteward convention.
  • Role splitonlyKeeper tunes within bounds; onlyOwner (governance) sets bounds, rotates the keeper, pauses, and has an incident-response setter that skips the cooldown/step throttle but is still boxed by [floor, ceiling].
  • Ownable2Step governance transfer; single lever (drawCap) on a single (hub, assetId, spoke) fixed at deploy; every check reads the live cap from the Hub as the source of truth.

Design notes

  • Sits alongside the whitelabel instance plumbing in feat: add etherfi spoke contract to restrict borrows to only etherfi safes #209 (EtherFiSpokeInstance, AaveV4RevenueSplitter); mirrors the splitter's shape (peripheral src/aave-v4/ contract, immutable config, inline minimal-interface slice, OZ access).
  • Complement to AccessManager, not a replacement: AccessManager gates who / which selector / with what delay; it does not bound the argument value — that's this contract's job.
  • Part of the ether.fi Lend project (COR-954); branches from and targets feat/lend per the base-branch workflow in feat: ether.fi Lend #153.

Test plan

  • FOUNDRY_PROFILE=lend forge test --match-contract CashRiskSteward21 passing (19 unit/fuzz + 2 stateful invariant)
  • Fuzz (10k runs): any target from any starting cap ends unchanged or inside [floor, ceiling], never the uncapped sentinel
  • test_Keeper_StepScalesWithCap: the same bps permits a 10M move at a 50M cap but only 2M at a 10M cap — proves the step scales with volume
  • Stateful invariant (keeper sequences + adversary bypass + adversarial ratchet + time warps): live drawCap never leaves the band
  • Mutation-verified: removing the ceiling check fails invariant_DrawCapStaysInBand (the geometric ratchet breaches > 90M) — proves the suite isn't vacuous
  • Gap reproduced: test_RawRoleHolder_CanUncapToUnlimited / CanFreezeBorrows demonstrate the raw-selector danger this wrapper removes
  • Deploy script + AccessManager role wiring (setTargetFunctionRole + grantRole) — follow-up
  • Fork test against the dev instance (deployments/dev/10/aave-v4-test.json) — follow-up

Note: the bound values in tests (1M floor / 90M ceiling / 20% step / 1h cooldown) are illustrative, not proposed production settings.

Bounds-enforcing wrapper around HubConfigurator.updateSpokeDrawCap so a
low-trust keeper can tune the Cash spoke's borrow cap within an on-chain
band it can never leave. Closes the Aave v4 gap where the drawCap value is
unbounded: a raw role-holder could set it to 0 (freeze borrows) or
MAX_ALLOWED_SPOKE_CAP (uncap -> 100% utilization). Grant the AccessManager
drawCap role to this contract instead of the keeper.

Includes unit + fuzz (10k runs) + stateful invariant tests with an
adversarial ratchet handler; the invariant is mutation-verified (removing
the ceiling check fails invariant_DrawCapStaysInBand).
@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

…t cap

Replace the absolute maxStep with maxStepBps (basis points of the current
drawCap), so the allowed per-update move scales with volume as the cap
grows — easier to interpret and operate (e.g. 500 = +/-5% per cooldown) and
matches Aave's RiskSteward convention. Bounds validation now requires
maxStepBps in (0, MAX_BPS]. Adds test_Keeper_StepScalesWithCap; invariant
handler ratchet/in-band math updated to the percentage form and remains
mutation-verified (ceiling removed still breaches invariant_DrawCapStaysInBand).
@seongyun-ko seongyun-ko changed the title feat: add CashRiskSteward for bounded Aave v4 drawCap tuning feat: add CashRiskSteward for bounded, rate-limited Aave v4 drawCap tuning Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant