Skip to content

Latest commit

 

History

History
200 lines (122 loc) · 16.3 KB

README.md

File metadata and controls

200 lines (122 loc) · 16.3 KB

Morpho Monitoring

Governance

For roles on Morpho vaults, refer to the following document.

Morpho governance monitoring is defined in the Python script that is executed daily via the monitoring runner because minimum timelock is 3 days to get vaults whitelisted, and 1 day is mimimum value in the contract.

The script checks if there are any new values pending in the timelock for a given vault. It detects the following changes:

  • Changing timelock value, minimal values is 1 day.
  • Changing guardian address.
  • Changing supply caps, only to higher value than the current one, for both supply and withdraw markets.
  • Removing of a market from the vault.

How to Add a New Vault

Add a VaultConfig to VAULTS_V1_BY_CHAIN in config.py. Every configured V1 vault receives both market and governance monitoring. governance.py iterates every chain in that mapping, so a new chain needs no script change; a failure on one chain is logged and the remaining chains are still checked before the run fails.

Monitored chains: Mainnet, Base, Katana, HyperEVM, and Robinhood Chain (V2 only). HyperEVM uses PROVIDER_URL_HYPEREVM when set; if no PROVIDER_URL_HYPEREVM* variable exists it falls back to the public https://rpc.hyperliquid.xyz/evm (PUBLIC_RPC_URLS in utils/chains.py). RPC requests time out after 60 seconds so a stalled public endpoint cannot hang the daily profile.

Vaults & Markets

Morpho Vaults consist of multiple markets, each defining key parameters such as LTV, interest rate models, and oracle data.

Market monitoring is configured through config.py, while shared market and liquidity policy lives in risk.py. Vaults with TVL below MIN_VAULT_ASSETS_USD ($10k) are skipped. The script fetches all markets for each vault and checks the following metrics:

  • Bad Debt Ratio: If the bad debt ratio exceeds 0.5% of total borrowed assets, a Telegram message is sent.
  • Vault Risk Level: If the computed risk level of a vault exceeds its maximum threshold, a Telegram message is sent.
  • Market Allocation Ratio: If any market's allocation ratio exceeds its risk-adjusted threshold, a Telegram message is sent.

Additional insights on Morpho vault risks are available at Llama Risk blog.

Risk Levels

The overall risk level of a Morpho Vault is determined by the risk levels of its markets. Markets and thresholds are defined in risk.py; vault scores are defined in config.py. Both are mutable operating configuration and are intentionally not fixed by tests. Level 1 represents the safest configuration.

Oracle validation

When adding or checking market rows in risk.py, use morpho-oracle-validation.md: resolve uniqueKey and oracle.address via the Morpho GraphQL API, validate feeds on-chain (typically MorphoChainlinkOracleV2 getters and description()), then classify feeds (Chainlink, RedStone, Chronicle, API3, or unknown) using on-chain hints plus official listings — Chainlink / data.chain.link, RedStone, Chronicle Oracles, Api3 Market (see §4 in that doc).

How to Add a New Vault

To monitor a new Morpho vault, add a VaultConfig to VAULTS_V1_BY_CHAIN or VAULTS_V2_BY_CHAIN in config.py. This is the single source used by market and governance monitoring.

For YV Collateral Vaults: Set collateral_asset on each V1 or V2 VaultConfig used by the Yearn strategy. Vaults with the same underlying asset are grouped automatically for combined liquidity monitoring.

Bad Debt

Bad debt is fetched from the Morpho GraphQL API. Each market is checked for bad debt; if any market exhibits bad debt, a Telegram message is sent. The script runs hourly via the monitoring runner. The monitoring logic is implemented in markets.py.

Liquidity

The standard check alerts when immediately withdrawable vault liquidity falls below a share of vault assets set in risk.py: 2% for both V1 (LIQUIDITY_THRESHOLD) and V2 (V2_LIQUIDITY_THRESHOLD).

V2 liquidity adapter check. A Vault V2 serves withdrawals only from idle assets plus its single liquidity adapter: withdraw deallocates from the adapter's market (liquidityData), not from every allocation. markets_v2.py therefore:

  1. Fetches the vault's active liquidityAdapter and its market (liquidityData).
  2. Takes the vault's position in that market from the adapter's positions and caps it by the market's free cash: idle + min(adapter supply, market cash).
  3. Alerts when that is below 2% of vault total assets.

This applies to every monitored V2 vault, including YV-collateral strategy vaults (which additionally get the coverage check below). The computed value matches the API's liquidityUsd and on-chain reads. Alerts show the idle/adapter split, the adapter, and the liquidity market's vault supply, cash and utilization. A vault with no liquidity adapter can only pay out idle assets. An adapter whose market or positions cannot be resolved fails the run instead of being treated as zero liquidity.

V1 YV-collateral strategy vaults use the market-aware coverage check below instead of the standard percentage threshold.

YV Collateral Vault Liquidity Monitoring

For vaults that are used as collateral in Yearn v3 strategies (YV collateral vaults), the system implements market-aware unwind liquidity monitoring. Instead of checking each vault individually, vaults with the same underlying asset are grouped together and their withdrawable liquidity is aggregated.

Configuration: YV collateral strategy vaults set collateral_asset in config.py. Keep both generations configured while liquidity migrates from V1 to V2. Watched direct YV-collateral markets are defined in YV_COLLATERAL_MARKETS_BY_ASSET in the same file.

Thresholds:

  • Regular vaults: LIQUIDITY_THRESHOLD (V1, 2%) and V2_LIQUIDITY_THRESHOLD (V2, 2%; also applied to YV-collateral V2 vaults) in risk.py.
  • YV collateral vaults: Require enough combined withdrawable liquidity to cover collateral at risk in direct YV-collateral Morpho markets, plus a liquidation buffer (YV_COLLATERAL_* constants in markets.py). Price shock is selected from market LLTV: 2% for LLTV >= 86%, 15% for LLTV <= 77%, otherwise 10%.

Logic: For each asset group (e.g., all USDC vaults at one chain), the system:

  1. Calculates combined total assets across all configured v1 and v2 strategy vaults with the same asset
  2. Calculates combined immediately withdrawable liquidity across those vaults. Shared Morpho market liquidity is capped once per market so v1 and v2 vaults cannot double-count the same cash. For v2 this uses the API's liquidityUsd value (idle assets plus the selected liquidity adapter) and conservatively excludes force-deallocatable liquidity
  3. Queries configured direct Yearn vault collateral markets such as yvvbUSDC/vbUSDT independently of vault allocations, so a v1 allocation removal cannot disable the check
  4. Fetches Morpho collateral-at-risk data at the configured adverse price shock
  5. Sums collateral at risk per underlying asset group
  6. Sends alerts only if combined withdrawable liquidity is below total collateral at risk plus buffer

Routing: unwind liquidity alerts go to the internal curation chat (TELEGRAM_CHAT_ID_CURATION), not the public morpho group — the fix is a curation decision (rebalance the vaults, adjust market caps). If that chat id is unset they fall back to the morpho group. Every other morpho alert is unaffected.

This approach alerts on liquidation coverage risk instead of low liquidity as a percentage of all assets, which avoids noise when a vault group is highly utilized but still has enough liquidity to unwind risky positions.

Vault Risk Level

The total risk level of a vault is computed as the weighted sum of the risk levels of its individual market allocations:

$$\text{Total Risk Level} = \sum_{i=1}^{n} (\text{Market Risk Level}_i \times \text{Allocation}_i)$$

Where:

  • Market Risk Level: A value between 1 and 5, with 1 representing the lowest risk. This value acts as a multiplier (e.g., a market with risk level 1 contributes a multiplier of 1, level 2 contributes 2, etc.).
  • Allocation: The percentage of the vault's assets allocated to that market.
  • Total Risk Level: The sum of the weighted risks across all markets.

This computed risk level is compared against the mutable MAX_RISK_THRESHOLDS configuration in risk.py.

The current maximum total-risk thresholds are:

  • Risk Level 1: 1.15
  • Risk Level 2: 2.30
  • Risk Level 3: 3.45
  • Risk Level 4: 4.60
  • Risk Level 5: 5.00

For example, a Risk Level 2 vault alerts when its weighted total risk exceeds 2.30. These values are a snapshot for readability; risk.py remains the source of truth and may be updated as risk assessments change.

If a vault's total risk level exceeds its threshold, an alert is triggered via a Telegram message.

Market Allocation Ratio

The system monitors each market's allocation within a vault to ensure it does not exceed its risk-adjusted threshold. Each market has a maximum allocation threshold based on its inherent risk tier and the vault's overall risk level.

The mutable base allocation limits and vault-level adjustment are defined by ALLOCATION_TIERS and get_market_allocation_threshold in risk.py. Higher-risk vault configurations can accept more exposure to a given market tier.

The current base limits are:

  • Risk Level 1 market: 101% configured, effectively allowing a full allocation
  • Risk Level 2 market: 30%
  • Risk Level 3 market: 10%
  • Risk Level 4 market: 5%
  • Risk Level 5 or unknown market: 1%

The vault risk level reduces the market tier used to select the limit by one step for each level above Risk Level 1, with a floor of Risk Level 1. For example:

  • A Risk Level 1 vault may allocate up to 30% to a Risk Level 2 market.
  • A Risk Level 2 vault may allocate fully to a Risk Level 2 market because its adjusted market tier is Risk Level 1.
  • A Risk Level 2 vault may allocate up to 10% to a Risk Level 4 market.
  • A Risk Level 3 vault may allocate up to 30% to a Risk Level 4 market.

Allocation ratios are capped at 100% by the monitor, so the configured 101% Risk Level 1 limit acts as a full-allocation allowance. As with the total-risk thresholds, risk.py is the source of truth for these mutable values.

The system monitors the allocation ratio for each market hourly:

$$\text{Allocation\_ratio} = \frac{\text{Market Supply USD}}{\text{Total Vault Assets USD}}$$

If any market's allocation exceeds its adjusted threshold, an alert is triggered with a corresponding Telegram message. This mechanism ensures that vaults maintain proper diversification and are not overly concentrated in higher-risk markets.

Vault V2 Monitoring

Morpho's Vault V2 replaces the v1 single-vault timelock with a per-function timelock keyed by arbitrary calldata, plus a richer adapter system. Yearn-curated v2 vaults are monitored separately:

  • governance_v2.py — daily, pulls a per-vault governance snapshot from Morpho's GraphQL API (vaultV2s.pendingConfigs + owner / curator / sentinels / allocators / adapters) and diffs it against the persisted cache. Mirrors v1's pull-based approach (pendingTimelock / pendingGuardian / pendingCap) so RPC usage stays bounded. Alerts on: new pending timelocked operations, executed or revoked operations, owner / curator changes, sentinel / allocator / adapter set changes.
  • markets_v2.py — hourly, GraphQL-only (no RPC): one vaultV2s query loads TVL, liquidity, and MorphoMarketV1 adapter positions for every configured vault, then one markets query per chain loads state/bad debt. Applies the shared risk.py policy using each position's supplyAssetsUsd, and checks withdrawable liquidity (idle plus the vault's position in the liquidity adapter's market, capped by market cash) against the 2% V2 threshold for every vault, including YV-collateral ones, which markets.py also covers with the combined collateral-at-risk check. Non-MorphoMarketV1 adapters fail the run (configured vaults are market-adapter only).
  • v2_decoders.py — selector→signature map and decoders for every v2 timelocked function (and the three idData tag prefixes used by increaseAbsoluteCap/increaseRelativeCap). Absolute caps are denominated in the vault's asset whatever id they are keyed by, so the decoder takes the vault's asset decimals/symbol; type(uint128).max renders as unlimited.

Grouped alerts

Both governance monitors buffer their findings per vault in _alerts.py and send them as one Telegram message per vault — one header naming the vault and chain, one severity (the highest of the group), sections separated by ---. A vault with several simultaneous changes no longer produces a burst of near-identical messages. Groups too long for a single message split into numbered (i/N) parts rather than being truncated, and the cache cursors that record "we alerted on this" are committed only after the send succeeds, so a Telegram failure is retried on the next run instead of being lost.

Vault list

Monitored V1 and V2 vaults live in config.py as typed VaultConfig rows. The V2 list is sourced from Yearn's curator page on Morpho and remains explicit so a third party using a similar name is not monitored automatically.

To add a new v2 vault, append a row to the chain's list and pick a risk tier (1–5).

A V2 vault that allocates only through a MetaMorpho adapter into a V1 vault (e.g. OUSD V2 on HyperEVM) sets monitor_markets=False: governance_v2.py still watches it, while markets_v2.py skips it because the wrapped V1 vault must be in VAULTS_V1_BY_CHAIN, where its markets are checked.

V2 monitoring needs no RPC, so a new chain only needs a Chain entry with its explorer URL in utils/chains.py and, if Morpho's URL slug differs from the enum name (e.g. robinhood-chain), a case in get_chain_name in _shared.py.

Cache

Both scripts share MORPHO_FILENAME (default cache-id.txt). New key types added by v2:

Key suffix Segment Value Meaning
v2_pending keccak(data) validAt ts, -1, or 0 Pending timelock operation: pending / executed / revoked
v2_pending_function keccak(data) function name Human-readable operation name used when a pending operation later executes or is revoked
v2_pending_index pending_keys comma-joined keccak(data) Reverse index used to detect operations that disappeared from pendingConfigs
v2_role owner / curator lowercase address Last-known instant-role address
v2_set sentinels / allocators / adapters comma-joined lowercase addresses Last-known role set

Limitations

MorphoMarketV1AdapterV2 has its own internal timelock system (setSkimRecipient, burnShares, increaseTimelock, etc.). The Morpho GraphQL API does not expose adapter-internal pending operations, and replaying Submit events on each adapter would reintroduce the RPC cost we're explicitly avoiding. Phase-2 candidate.

How to run locally

# Hourly (allocation + risk)
python protocols/morpho/markets_v2.py
# Daily (timelocks + role changes)
python protocols/morpho/governance_v2.py

Set MORPHO_FILENAME=/tmp/morpho-cache.txt to use an isolated cache while testing.

API Docs

Morpho GraphQL API wizard is available at https://api.morpho.org/graphql. GraphQL schema is available in morpho_ql_schema.txt file. For fetching a market oracle and validating it against RPC (including Chainlink, RedStone, Chronicle, API3), see morpho-oracle-validation.md.