Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion monitoring.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ protocols:

morpho:
display_name: "Morpho"
description: "Morpho market, vault, and governance monitoring on Mainnet, Base, Katana, and HyperEVM"
description: "Morpho market, vault, and governance monitoring on Mainnet, Base, Katana, HyperEVM, and Robinhood Chain"
cadence: "Hourly / Daily"
tasks:
- protocols/morpho/markets.py
Expand Down
4 changes: 3 additions & 1 deletion protocols/morpho/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The script checks if there are any new values pending in the timelock for a give

Add a `VaultConfig` to `VAULTS_V1_BY_CHAIN` in [config.py](./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, and HyperEVM. 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](../../utils/chains.py)). RPC requests time out after 60 seconds so a stalled public endpoint cannot hang the daily profile.
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](../../utils/chains.py)). RPC requests time out after 60 seconds so a stalled public endpoint cannot hang the daily profile.

## Vaults & Markets

Expand Down Expand Up @@ -156,6 +156,8 @@ To add a new v2 vault, append a row to the chain's list and pick a risk tier (1

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](../../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](./_shared.py).

### Cache

Both scripts share `MORPHO_FILENAME` (default `cache-id.txt`). New key types added by v2:
Expand Down
2 changes: 2 additions & 0 deletions protocols/morpho/_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def get_chain_name(chain: Chain) -> str:
"""Return the chain segment used in Morpho frontend URLs."""
if chain == Chain.MAINNET:
return "ethereum"
if chain == Chain.ROBINHOOD:
return "robinhood-chain"
return str(chain.name).lower()


Expand Down
1 change: 1 addition & 0 deletions protocols/morpho/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class VaultConfig:
# Allocates only through a MetaMorpho adapter into the HyperEVM OUSD V1 vault.
VaultConfig("OUSD V2", "0xE90959cbE7E56b5eBFF9AD12de611A4976F2d2B1", 2, monitor_markets=False),
),
Chain.ROBINHOOD: (VaultConfig("Yearn Stonk USDG", "0x8c258F25fc6fBe3f01ccC828E2C3840A46a4a9A2", 3),),
Chain.KATANA: (
VaultConfig("Yearn OG USDC", "0xca44cbe1FB03691d43d2d93AA460e2fCB03878fE", 1, KATANA_USDC),
VaultConfig("Yearn OG USDT", "0x4284d4F9f4d61eA57B8F0943547c7C19C5B9B249", 1, KATANA_USDT),
Expand Down
8 changes: 8 additions & 0 deletions protocols/morpho/risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@
"0xe7aa046832007a975d4619260d221229e99cc27da2e6ef162881202b4cd2349b", # kHYPE/USDC -> lltv 62.5%, oracle: MorphoChainlinkOracleV2 — RedStone kHYPE_FUNDAMENTAL (kHYPE/HYPE) × RedStone HYPE/USD; no quote feed (USDC = $1).
"0x96c47d3797394dd25e33f814216793a89d2e6edcc54d1dac3ab4c2b2d82a73b9", # kHYPE/USDC -> lltv 77%, oracle: MorphoChainlinkOracleV2 — RedStone kHYPE/USD ÷ RedStone USDC/USD (spot).
],
Chain.ROBINHOOD: [
"0x50bc39b5722fb5634c436d74c6787f3c125b879e7b73cf9e9ecc01bbb57b8e55", # SPY/USDG -> lltv 62.5%, oracle: MorphoChainlinkOracleV2 — Chainlink RHSPY/USD and Chainlink USDG/USD. Broad index ETF; equity feeds only move in trading hours, so weekend gaps are absorbed by the low LLTV.
],
}

MARKETS_RISK_3 = {
Expand Down Expand Up @@ -200,6 +203,11 @@
"0x8147c63f3f6f5a0825c84bf2cb11443c72b609fa39cf9a362e3d4dc2c5ca76c4", # PT-USDai-19FEB2026/USDC -> lltv 91.5%, oracle: MetaOracleDeviationTimelock by Steakhouse whose primary and backup both read the same Pendle Chainlink-compatible PT feed https://arbiscan.io/address/0x7bfCE6c9717F29dcffAAE3a96DE10c379fE97F88 (900s AMM TWAP) where USDai=USDC, so the deviation check moves in lockstep with a manipulated PT price instead of catching it.
"0x7717f1e04510390518811b3133ea47c298094ddd1d806ed8f8867d88c727bad7", # PT-sUSDai-19FEB2026/USDC -> lltv 86%, oracle: Pendle Chainlink-compatible oracle (PT to asset, 900s AMM TWAP) for sUSDai with ERC4626 vault rate sUSDai to USDai. Chainlink oracle USDC/USD.
],
Chain.ROBINHOOD: [
"0x69400cfe81f2ae381b9a9f27076a4d637338379d999e9ffdfbde52f0876312ee", # META/USDG -> lltv 62.5%, oracle: MorphoChainlinkOracleV2 — Chainlink Robinhood META/USD and Chainlink USDG/USD. Single stock, gap risk outside trading hours.
"0x66306c087add8907752320b309934abcc354d21626de8115c79df49d9c214edc", # NVDA/USDG -> lltv 62.5%, oracle: MorphoChainlinkOracleV2 — Chainlink RHNVDA/USD and Chainlink USDG/USD. Single stock, gap risk outside trading hours.
"0x21ea2a012b888120e328775c2a7bf6fb4619cf4c4c631ad1c9b6db51f8882d5a", # SPCX/USDG -> lltv 62.5%, oracle: MorphoChainlinkOracleV2 — Chainlink Robinhood SPCX/USD and Chainlink USDG/USD. Single stock, gap risk outside trading hours.
],
}

MARKETS_RISK_4 = {
Expand Down
16 changes: 16 additions & 0 deletions tests/test_morpho_markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
MarketState,
MorphoMonitoringError,
MorphoV2MonitoringError,
get_vault_url,
)
from protocols.morpho.config import VAULTS_V2_BY_CHAIN, VaultConfig, get_collateral_vaults_by_asset
from protocols.morpho.markets import (
Expand Down Expand Up @@ -470,6 +471,21 @@ def test_market_risk_level_is_case_insensitive_and_defaults_to_5(self) -> None:
self.assertEqual(get_market_risk_level(market_id, Chain.MAINNET), 5)
self.assertEqual(get_market_risk_level("0x" + "00" * 32, Chain.HYPEREVM), 5)

def test_robinhood_stonk_vault_markets_are_tiered(self) -> None:
spy = "0x50bc39b5722fb5634c436d74c6787f3c125b879e7b73cf9e9ecc01bbb57b8e55"
nvda = "0x66306c087add8907752320b309934abcc354d21626de8115c79df49d9c214edc"

self.assertEqual(get_market_risk_level(spy, Chain.ROBINHOOD), 2)
self.assertEqual(get_market_risk_level(nvda, Chain.ROBINHOOD), 3)


class TestMorphoUrls(unittest.TestCase):
def test_robinhood_uses_morpho_chain_slug(self) -> None:
vault = "0x8c258F25fc6fBe3f01ccC828E2C3840A46a4a9A2"

self.assertEqual(get_vault_url(vault, Chain.ROBINHOOD), f"https://app.morpho.org/robinhood-chain/vault/{vault}")
self.assertEqual(Chain.ROBINHOOD.explorer_url, "https://robinhoodchain.blockscout.com")


if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions utils/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
137: "https://polygonscan.com",
8453: "https://basescan.org",
999: "https://hyperevmscan.io",
4663: "https://robinhoodchain.blockscout.com",
42161: "https://arbiscan.io",
747474: "https://katanascan.com",
}
Expand All @@ -24,6 +25,7 @@ class Chain(Enum):
ARBITRUM = (42161, "arbitrum", "arb")
POLYGON = (137, "polygon", "matic")
HYPEREVM = (999, "hyperevm", "hype")
ROBINHOOD = (4663, "robinhood", "eth")
KATANA = (747474, "katana", "kat")

def __init__(self, chain_id: int, network_name: str, symbol: str):
Expand Down
Loading