morpho: add V2 vault monitoring (timelocks + allocations)#213
Open
morpho: add V2 vault monitoring (timelocks + allocations)#213
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
morpho/governance_v2.py(daily) andmorpho/markets_v2.py(hourly), wired into the existing workflows.vaultV2sand matched by name against the existing v1VAULTS_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 replaySubmit/Accept/Revokeevents on each vault and on eachMorphoMarketV1AdapterV2(adapters have their own timelock system). Each Submit's calldata is decoded into a human-readable Telegram alert viamorpho/v2_decoders.py, which knows all 20 timelocked selectors plus per-arg formatters — including the threeidDatatag 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 asMorphoMarketV1AdapterV2(probemarketIdsLength()) orMorphoVaultV1Adapter, then:expectedSupplyAssets(marketId)per market, fetch market metrics from GraphQL, and run the existing v1 risk-tier scoring (MARKETS_RISK_*+ALLOCATION_TIERS+MAX_RISK_THRESHOLDSare imported frommarkets.py, not duplicated).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_FILENAMEwith three new key types:v2_submit(pending/executed/revoked),v2_instant(owner action seen),v2_block(resume point per address). Addsutils/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.pyuv run ruff check morpho/ utils/cache.py tests/test_morpho_v2_decoders.py— cleanuv run mypy --explicit-package-bases morpho/_shared.py morpho/v2_decoders.py morpho/markets_v2.py morpho/governance_v2.py— clean on new filesuv run pytest tests/test_morpho_v2_decoders.py— 24 / 24 golden decoder tests pass (one per selector + idData prefix)LOOKBACK_BLOCKS)Files
morpho/{_shared.py, v2_decoders.py, markets_v2.py, governance_v2.py}, three ABIs inmorpho/abi/,tests/test_morpho_v2_decoders.pyutils/cache.py(cache helpers),morpho/README.md(V2 section),.github/workflows/{hourly,daily}.yml(added v2 entrypoints)🤖 Generated with Claude Code