Skip to content

feat(morpho): critical alerts for market removals that can lose vault funds - #436

Open
spalen0 wants to merge 4 commits into
mainfrom
market-removal
Open

spalen0 wants to merge 4 commits into
mainfrom
market-removal

Conversation

@spalen0

@spalen0 spalen0 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #434

What

Curator actions that can write off vault funds now send a CRITICAL alert for any non-zero amount. Each goes out as its own 🚨 POTENTIAL VAULT LOSS message on the morpho channel, and is re-alerted once within 24h of its execution time.

Vault Operation CRITICAL when Detected via
V1 submitMarketRemoval the vault still supplies the market config.removableAt + Morpho Blue position/market
V2 removeAdapter(adapter) the adapter holds assets GraphQL pendingConfigs + adapter assets
V2 decreaseTimelock(removeAdapter, …) any adapter holds assets GraphQL pendingConfigs
V2 adapter burnShares(marketId) the adapter holds that market on-chain executableAt(burnShares(id))

Other changes:

  • V1 timelock: the alert now reads Timelock decrease: 3d → 1d, with the execution time and a countdown. It used to say just "Timelock is changing".
  • V2: other decreaseTimelock calls and abdicate are now HIGH (they were MEDIUM). removeAdapter(...) names the adapter again.
  • V2 adapter: a drop in timelock(burnShares) sends HIGH. The first run seeds the cache silently.
  • V1 empty-market removals stay MEDIUM.

How burnShares is detected (batched eth_call only)

The Morpho GraphQL API has no adapter-level timelock data, so each MorphoMarketV1AdapterV2 is read on-chain: executableAt(burnShares(id)) for every held market → CRITICAL with the amount at risk, plus a reminder within 24h. A drop in timelock(burnShares) → HIGH. Valuation calls run only when a burn is pending.

TODO: a burn submitted with non-canonical calldata (canonical + trailing bytes) is not detected. Solidity ignores the extra bytes, but executableAt is keyed by the exact calldata, so the key can't be derived. Catching it needs the adapter's Submit events, which the Morpho API does not expose. It can be added once an indexer provides them.

Amounts come from stored shares and Morpho Blue market totals, never the IRM (it can revert), and criticality follows shares. On V1, totalAssets() is best effort, so a broken IRM only drops the "% of vault" figure.

Robinhood had no RPC in the env or in PUBLIC_RPC_URLS, so I added its official public RPC (rpc.mainnet.chain.robinhood.com, listed on chainid.network) as a fallback.

The added GraphQL fields pushed first: 200 past Morpho's 1M complexity limit (1,151,000). The page size is now 100 (575,500), passed as a variable, and a test checks that the configured vaults fit.

Testing

  • ruff, ty, and the full test suite pass (1945 tests). New tests cover severity classification, standalone delivery, reminder-once logic, timelock decrease, and adapter executableAt reads. The fake client in the adapter tests rejects .call() outside a batch context; that is the bug the first live run caught.
  • Mainnet fork (anvil): confirmed the non-canonical calldata gap (the padded submit executes while the canonical key reads 0). It's documented as the TODO above.
  • Live dry run of both monitors against real chains, with alerts printed instead of sent and a temporary cache. Every chain passes, and the only RPC methods used are eth_blockNumber and eth_call. No pending loss operations exist today. The Katana Yearn OG USDC weETH/vbUSDC removal (no supply) correctly came out as MEDIUM. Every adapter returns timelock(burnShares) of 3d or 7d.

Rollout notes

Removals and removeAdapter operations that were already pending (and already alerted at MEDIUM) before this deploys won't trigger a new first alert. If they still have funds at risk, they'll get the CRITICAL reminder within 24h of execution.

🤖 Generated with Claude Code

spalen0 and others added 4 commits October 22, 2026 05:25
… funds

V1 submitMarketRemoval is now CRITICAL when the vault still supplies the
market (any amount), with the amount at risk and its share of totalAssets,
and re-alerted once within 24h of removableAt. Empty-market removals stay
MEDIUM. The V1 timelock alert now shows current -> new and executable time.

V2 removeAdapter of an adapter holding assets, and decreaseTimelock on
removeAdapter while adapters hold assets, are CRITICAL; other timelock
decreases and abdicate are HIGH. removeAdapter labels name the adapter again.

New adapter_timelocks_v2 reads MorphoMarketV1AdapterV2's own timelock
on-chain (not in the Morpho API): executableAt(burnShares(id)) for every
held market, plus a HIGH alert when timelock(burnShares) drops.

Loss findings are sent as their own "POTENTIAL VAULT LOSS" message on the
morpho channel. Adds Robinhood's official public RPC as a fallback.

Refs #434

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Noncanonical calldata: executableAt is keyed by exact calldata and Solidity
  ignores trailing bytes, so a padded burnShares submission evaded the
  canonical-key check (reproduced on a mainnet fork: padded submit pending,
  canonical key 0, padded call burned the shares). Pending burns are now also
  discovered from the adapter's Submit(burnShares) logs, scanned incrementally
  from its creation block with a per-adapter cursor; every discovered calldata
  is kept verbatim and re-read until it resolves. Canonical keys stay as a fast
  path, and a failed or incomplete scan fails the run after alerting.
- Valuation: burned-market amounts use stored supplyShares and Morpho Blue
  market totals instead of expectedSupplyAssets, which goes through the IRM
  and may revert; criticality follows shares.
- V1: totalAssets() is best effort, so a reverting withdraw-queue IRM only
  drops the "% of vault" figure instead of aborting governance checks.
- V2 governance query: page size 100 (the added fields pushed first: 200 past
  Morpho's 1M complexity limit), passed as a variable and guarded by a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getLogs history scans are too expensive for our RPC budget. The adapter
check now uses batched eth_call only:

- pending burns: canonical executableAt(burnShares(id)) per held market;
- executed burns: adapter supplyShares[id] always equals its Morpho Blue
  position (verified on all 50 live positions), and burnShares zeroes only
  the former, so any gap is a written-off position -> CRITICAL. This
  catches burns submitted with padded calldata once they execute
  (fork-tested); markets are remembered after leaving marketIds.

Known limitation, documented: a padded-calldata burn is not visible while
pending without Submit events. Removes the log cursor, chunk config, and
the adapter creationBlockNumber query field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the share-parity check (extra Morpho position/market reads per market
and a remembered-markets cache). Non-canonical burnShares submissions need
the adapter's Submit events, which the Morpho API does not expose; that gap
is now a documented TODO. Valuation calls run only when a canonical burn is
pending.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gjwang

gjwang commented Oct 23, 2026

Copy link
Copy Markdown

Could you add a zero-to-positive shares transition test for the same pending burn?

At d14ab1d3, lines 233–237, the dedup check only remembers executable_at. If a zero-share burn first sends MEDIUM at T−72h, then has positive shares at T−48h, the unchanged timestamp takes the early continue: no CRITICAL until the reminder window. Tracking whether a critical alert was delivered for that pending operation would distinguish escalation from duplicates.

I reproduced that sequence with the extracted function and mocked cache, clock and delivery; a clean-cache positive-share control sends CRITICAL. I have not run the full suite or a live-chain test. The existing tests cover zero and positive shares separately; the transition would catch this gap.

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.

Critical alert for Morpho market removals that can lose vault funds (V1 submitMarketRemoval, V2 removeAdapter / burnShares)

2 participants