Skip to content

morpho: add V2 vault monitoring (timelocks + allocations)#213

Open
spalen0 wants to merge 6 commits intomainfrom
feat/morpho-v2-monitoring
Open

morpho: add V2 vault monitoring (timelocks + allocations)#213
spalen0 wants to merge 6 commits intomainfrom
feat/morpho-v2-monitoring

Conversation

@spalen0
Copy link
Copy Markdown
Collaborator

@spalen0 spalen0 commented Apr 29, 2026

Summary

  • Adds parallel monitoring for Morpho VaultV2 — a redesign of MetaMorpho with a per-function timelock keyed by arbitrary calldata and a richer adapter system. V1 monitoring is left untouched.
  • New entrypoints: morpho/governance_v2.py (daily) and morpho/markets_v2.py (hourly), wired into the existing workflows.
  • Vaults are discovered via Morpho's GraphQL vaultV2s and matched by name against the existing v1 VAULTS_BY_CHAIN. Live discovery currently picks up 13 V2 vaults across MAINNET/BASE/KATANA (incl. Gauntlet USDC Prime $92M, Yearn OG USDC, Moonwell flagships).

Timelock monitoring (governance_v2.py)

V2's executableAt[bytes data] mapping is not enumerable, so we replay Submit / Accept / Revoke events on each vault and on each MorphoMarketV1AdapterV2 (adapters have their own timelock system). Each Submit's calldata is decoded into a human-readable Telegram alert via morpho/v2_decoders.py, which knows all 20 timelocked selectors plus per-arg formatters — including the three idData tag prefixes ("this", "collateralToken", "this/marketParams"). Owner-instant changes (SetOwner/SetCurator/SetIsSentinel) and audit events (AddAdapter/RemoveAdapter/IncreaseTimelock/DecreaseTimelock/Abdicate) are also alerted.

Allocation & risk monitoring (markets_v2.py)

Per vault: read adapters() on-chain, classify each as MorphoMarketV1AdapterV2 (probe marketIdsLength()) or MorphoVaultV1Adapter, then:

  • For market-v1 adapters: read expectedSupplyAssets(marketId) per market, fetch market metrics from GraphQL, and run the existing v1 risk-tier scoring (MARKETS_RISK_* + ALLOCATION_TIERS + MAX_RISK_THRESHOLDS are imported from markets.py, not duplicated).
  • For vault-v1 adapters: confirm the wrapped v1 vault is already in v1 VAULTS_BY_CHAIN; alert once if a new unknown v1 vault is wrapped.
    Bad debt and utilization use the same v1 GraphQL pipeline. Liquidity is deferred to phase 2 (TODO documented).

Cache

Reuses MORPHO_FILENAME with three new key types: v2_submit (pending/executed/revoked), v2_instant (owner action seen), v2_block (resume point per address). Adds utils/cache.py:get_last_processed_block / write_last_processed_block.

Test plan

  • uv run ruff format morpho/ utils/cache.py tests/test_morpho_v2_decoders.py
  • uv run ruff check morpho/ utils/cache.py tests/test_morpho_v2_decoders.py — clean
  • uv run mypy --explicit-package-bases morpho/_shared.py morpho/v2_decoders.py morpho/markets_v2.py morpho/governance_v2.py — clean on new files
  • uv run pytest tests/test_morpho_v2_decoders.py24 / 24 golden decoder tests pass (one per selector + idData prefix)
  • Live GraphQL discovery dry-run finds 13 matched V2 vaults across 3 chains
  • Reviewer: confirm Telegram alerts render correctly during the first daily run (governance_v2.py is event-replay-based, so the first run on each vault will emit any historical pending Submits within LOOKBACK_BLOCKS)
  • Reviewer: confirm hourly run discovers and reads adapter state without RPC errors

Files

  • New: morpho/{_shared.py, v2_decoders.py, markets_v2.py, governance_v2.py}, three ABIs in morpho/abi/, tests/test_morpho_v2_decoders.py
  • Modified: utils/cache.py (cache helpers), morpho/README.md (V2 section), .github/workflows/{hourly,daily}.yml (added v2 entrypoints)

🤖 Generated with Claude Code

spalen0 and others added 6 commits April 29, 2026 15:09
Adds parallel monitoring for Morpho VaultV2, which uses a per-function
timelock keyed by arbitrary calldata and a richer adapter system. v1
monitoring is left untouched.

* governance_v2.py — daily, replays Submit/Accept/Revoke events on each
  vault and on each MorphoMarketV1AdapterV2 adapter, decodes embedded
  calldata into Telegram alerts, and flags owner-instant changes
  (SetOwner/SetCurator/SetIsSentinel).
* markets_v2.py — hourly, walks each vault's adapters on-chain and runs
  the existing v1 risk-tier scoring against underlying Morpho Blue
  markets when the vault uses MorphoMarketV1AdapterV2.
* v2_decoders.py — selector→signature map (20 entries) with per-arg
  formatters, including idData decoders for the three cap-id tag
  prefixes ("this", "collateralToken", "this/marketParams").
* Vault discovery via Morpho GraphQL `vaultV2s`, name-matched against
  v1 VAULTS_BY_CHAIN with risk inheritance — paginated to stay under
  the API's 1M complexity limit.
* utils/cache.py gains get_last_processed_block /
  write_last_processed_block for resumable event polling.
* 24 hermetic decoder tests covering every timelocked selector.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the loosely-typed Dict[str, Dict[str, Any]] return value with
typed dataclasses (Asset, MarketState, BadDebt, MarketMetrics) so
callers get IDE completion and mypy coverage on per-market fields.

Updates callers in markets_v2.py (_allocation_to_usd, _market_label,
bad-debt extraction) to use attribute access instead of dict lookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Don't advance the eth_getLogs checkpoint past a failed chunk —
  _get_logs now returns (logs, last_successful_block) and
  _process_target writes the minimum across all polled events,
  so transient RPC failures are retried instead of silently
  dropping Submit / Accept / Revoke alerts.
* Include the emitting contract address in the Submit cache key
  (vault vs adapter) so two adapters submitting identical calldata
  — e.g. increaseTimelock(addAdapter, …) on multiple market-v1
  adapters — don't mask each other.
* Aggregate per-market allocations across **all** market-v1
  adapters before scoring against MAX_RISK_THRESHOLDS. Splitting
  exposure across two adapters can no longer dodge the vault-level
  risk threshold.
* Refresh the stale VaultV2 section of morpho_ql_schema.txt to
  match the live API (curator/owner are nested Account, not
  curatorAddress/ownerAddress scalars). The discovery code in
  markets_v2.py was already correct against the live endpoint;
  the checked-in schema was outdated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the eth_getLogs-based timelock monitor with a single GraphQL
query per chain that reads VaultV2.pendingConfigs directly — same
pull-based approach v1's governance.py uses for pendingTimelock /
pendingGuardian / pendingCap. RPC usage drops from N adapters × M
events × ~5k-block chunks per run to a single GraphQL request per
script run.

What's covered
* Pending timelocked operations (vaultV2s.pendingConfigs): new
  submissions, executions, and revocations. Each is identified by
  keccak256(data) so adapters with identical calldata don't collide.
* Owner / curator changes (instant, no timelock).
* Sentinels / allocators / adapters: per-vault set diffs.

Not covered
* MorphoMarketV1AdapterV2's own internal timelock system. The
  GraphQL API does not surface adapter-internal pending operations
  and replaying their events would reintroduce the RPC cost we are
  explicitly avoiding. Phase-2 candidate, documented in README and
  module docstring.

Implementation notes
* `_shared.py` now hosts `normalize_vault_name` and
  `build_v1_name_index` so both governance_v2 and markets_v2 share
  one matcher.
* Cache scheme adjusts: `v2_pending` (validAt | -1 | 0),
  `v2_pending_index` (reverse index for resolution detection),
  `v2_role` (owner / curator), `v2_set` (sentinels / allocators /
  adapters). Drops `v2_block` and the get/write_last_processed_block
  helpers.
* Live dry-run finds 13 matched vaults across 3 chains and 18
  pending operations on one mainnet vault, all decoded correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The compound.blue frontend and Yearn-curated Compound vaults on Polygon
are deprecated. Cleanup:

* Remove COMPOUND_URL and Chain.POLYGON branches from get_market_url /
  get_vault_url in _shared.py and governance.py.
* Drop the three Polygon Compound vaults from governance.py
  VAULTS_BY_CHAIN and stop calling get_data_for_chain(Chain.POLYGON).
* Remove all Chain.POLYGON entries from markets.py risk maps
  (MARKETS_RISK_1 / 2 / 3 / 4 / 5).
* Delete morpho/markets_graph.py — placeholder file that only ever
  served the Compound/Polygon route, never wired into a workflow.
* Drop the corresponding markets_graph.py reference from the README.

Inline oracle annotations referring to Compound DAO's wstETH/ETH price
feed remain — those describe an on-chain oracle still used by
non-Compound markets, unrelated to the deprecated frontend.

Both v2 monitors still match 13 vaults across MAINNET/BASE/KATANA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces name-based GraphQL discovery (which couldn't reject squatters
who reused real curator names) with an explicit list of Yearn-curated
V2 vaults sourced from https://app.morpho.org/curator/yearn?v2=true.

* Static list lives in _shared.py as VAULTS_V2_BY_CHAIN — same shape as
  v1 markets.py:VAULTS_BY_CHAIN ([name, address, risk_level] rows). 13
  vaults across MAINNET (4), BASE (3), KATANA (6).
* markets_v2.py and governance_v2.py both import the list from _shared
  and join their per-script GraphQL response back to it; queries now
  use vaultV2s(where: { address_in, chainId_in }) instead of
  paginating + name-matching.
* Drops the v1-name-index helpers (build_v1_name_index,
  normalize_vault_name) — no longer needed.
* Drops the matched_v1_name field from V2Vault — never read.

Live dry-run loads all 13 vaults for both monitors and surfaces real
pending configs (BASE Yearn OG WETH V2 has 3 pending, BASE OUSD has 1,
KATANA Yearn KAT has 7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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