diff --git a/.github/workflows/_run-monitoring.yml b/.github/workflows/_run-monitoring.yml index 7d4959d..51de225 100644 --- a/.github/workflows/_run-monitoring.yml +++ b/.github/workflows/_run-monitoring.yml @@ -81,6 +81,7 @@ env: TELEGRAM_CHAT_ID_STARGATE: ${{ secrets.TELEGRAM_CHAT_ID_STARGATE }} TELEGRAM_CHAT_ID_USDAI: ${{ secrets.TELEGRAM_CHAT_ID_USDAI }} TELEGRAM_CHAT_ID_YEARN: ${{ secrets.TELEGRAM_CHAT_ID_YEARN }} + TELEGRAM_CHAT_ID_YEARN_MS: ${{ secrets.TELEGRAM_CHAT_ID_YEARN_MS }} # ── Telegram Topics (forum-style group routing) ── # These are non-sensitive config, so they use repo variables (vars.*) instead of secrets. diff --git a/morpho/markets.py b/morpho/markets.py index b2d0096..b511662 100644 --- a/morpho/markets.py +++ b/morpho/markets.py @@ -175,7 +175,6 @@ "0x84662b4f95b85d6b082b68d32cf71bb565b3f22f216a65509cc2ede7dccdfe8c", # cbETH/WETH -> lltv 94.5%, oracle: Chainlink cbETH-ETH Exchange Rate "0x5dffffc7d75dc5abfa8dbe6fad9cbdadf6680cbe1428bafe661497520c84a94c", # cbBTC/WETH -> lltv 91.5%, oracle: Chainlink BTC/USD and Chainlink ETH/USD "0xa7813c754ddd6a24e1a1a29ff3ea877803ac63d09efc2f121b1cf3f0bf3af2f6", # WETH/cbBTC -> lltv 91.5%, oracle: Chainlink ETH/USD and Chainlink BTC/USD - "0x78d11c03944e0dc298398f0545dc8195ad201a18b0388cb8058b1bcb89440971", # weWETH/WETH -> lltv 91.5%, oracle: Chainlink weETH/ETH exchange rate "0x3b3769cfca57be2eaed03fcc5299c25691b77781a1e124e7a8d520eb9a7eabb5", # USDC/WETH -> lltv 86.5%, oracle: Chainlink USDC/USD and Chainlink ETH/USD ], Chain.KATANA: [ diff --git a/safe/addresses.py b/safe/addresses.py new file mode 100644 index 0000000..df56ded --- /dev/null +++ b/safe/addresses.py @@ -0,0 +1,201 @@ +"""Static configuration for the Safe-multisig monitor. + +Split out from safe/main.py to keep that file focused on logic. +""" + +# Maps Safe API network names to short prefixes used in app.safe.global URLs. +safe_address_network_prefix = { + "mainnet": "eth", + "arbitrum-main": "arb1", + "optimism-main": "oeth", + "polygon-main": "matic", + "optim-yearn": "oeth", + "base-main": "base", + "katana-main": "katana", +} + +# Maps Safe API network names to their transaction-service base URL. +safe_apis = { + "mainnet": "https://api.safe.global/tx-service/eth", + "arbitrum-main": "https://api.safe.global/tx-service/arb1", + "optimism-main": "https://api.safe.global/tx-service/oeth", + "polygon-main": "https://api.safe.global/tx-service/pol", + "base-main": "https://api.safe.global/tx-service/base", + "katana-main": "https://api.safe.global/tx-service/katana", + # "optim-yearn": "https://safe-transaction-optimism.safe.global", +} + +PROXY_UPGRADE_SIGNATURES = [ + # Standard Proxy (OpenZeppelin, UUPS, Transparent) + "3659cfe6", # bytes4(keccak256("upgradeTo(address)")) + "4f1ef286", # upgradeToAndCall(address,bytes) + "f2fde38b", # changeProxyAdmin(address,address) + # Diamond Proxy (EIP-2535) + "1f931c1c", # diamondCut((address,uint8,bytes4[])[],address,bytes) +] + +# Watched non-yearn protocol multisigs. Format: [protocol, network, address, optional label]. +ALL_SAFE_ADDRESSES = [ + [ + "LIDO", + "mainnet", + "0x73b047fe6337183A454c5217241D780a932777bD", + ], # https://docs.lido.fi/multisigs/emergency-brakes/#12-emergency-brakes-ethereum + [ + "LIDO", + "mainnet", + "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C", + ], # https://docs.lido.fi/multisigs/emergency-brakes/#11-gateseal-committee -> expires on 1 April 2025. + ["PENDLE", "mainnet", "0x8119EC16F0573B7dAc7C0CB94EB504FB32456ee1"], + ["PENDLE", "arbitrum-main", "0x7877AdFaDEd756f3248a0EBfe8Ac2E2eF87b75Ac"], + ["EULER", "mainnet", "0xcAD001c30E96765aC90307669d578219D4fb1DCe"], + [ + "AAVE", + "mainnet", + "0x2CFe3ec4d5a6811f4B8067F0DE7e47DfA938Aa30", + ], # aave Protocol Guardian Safe: https://app.aave.com/governance/v3/proposal/?proposalId=184 + ["AAVE", "polygon-main", "0xCb45E82419baeBCC9bA8b1e5c7858e48A3B26Ea6"], + ["AAVE", "arbitrum-main", "0xCb45E82419baeBCC9bA8b1e5c7858e48A3B26Ea6"], + [ + "AAVE", + "mainnet", + "0xCe52ab41C40575B072A18C9700091Ccbe4A06710", + ], # aave Governance Guardian Safe + ["AAVE", "polygon-main", "0x1A0581dd5C7C3DA4Ba1CDa7e0BcA7286afc4973b"], + ["AAVE", "arbitrum-main", "0x1A0581dd5C7C3DA4Ba1CDa7e0BcA7286afc4973b"], + [ + "MORPHO", + "mainnet", + "0x84258B3C495d8e9b10D0d4A7867392F149Da4274", + "Morpho eUSDe predeposit vault owner", + ], # eUSDe predeposit vault owner, token used by DAI vault on morpho + [ + "LRT", + "mainnet", + "0xb7cB7131FFc18f87eEc66991BECD18f2FF70d2af", + "LBTC boring vault big boss", + ], # LBTC boring vault big boss + [ + "LRT", + "base-main", + "0x92A19381444A001d62cE67BaFF066fA1111d7202", + "Origin admin multisig. Markets used on Base", + ], # origin admin + [ + "LRT", + "mainnet", + "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f", + "tBTC bridge owner multisig. aka, Council Multisig", + ], # tBTC bridge owner multisig (Council Multisig) + [ + "USDAI", + "arbitrum-main", + "0xF223F8d92465CfC303B3395fA3A25bfaE02AED51", + "USDai Admin Safe", + ], + [ + "USDAI", + "arbitrum-main", + "0x783B08aA21DE056717173f72E04Be0E91328A07b", + "sUSDai Admin Safe", + ], + [ + "CAP MONEY", + "mainnet", + "0xb8FC49402dF3ee4f8587268FB89fda4d621a8793", + "Cap Money Multisig", + ], + [ + "MAPLE", + "mainnet", + "0xd6d4Bcde6c816F17889f1Dd3000aF0261B03a196", + "Maple DAO Multisig (syrupUSDC)", + ], + [ + "STRATA", + "mainnet", + "0xA27cA9292268ee0f0258B749f1D5740c9Bb68B50", + "Strata Admin Multisig (3/4)", + ], + # [ + # "INFINIFI", + # "mainnet", + # "0x80608f852D152024c0a2087b16939235fEc2400c", + # "Infinifi Team Multisig", + # ], +] + +# Yearn-controlled multisigs. Source: +# https://gist.githubusercontent.com/engn33r/c02a3a511a2ffdd1fe5453640e155b40/raw/multisigs.csv +# Limited to mainnet/base/katana (other chains intentionally excluded). +YEARN_MULTISIGS: list[list[str]] = [ + ["YEARN_MS", "mainnet", "0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52", "yChad (Yearn multisig/daddy)"], + ["YEARN_MS", "base-main", "0xbfAABa9F56A39B814281D68d2Ad949e88D06b02E", "bChad Multisig"], + ["YEARN_MS", "katana-main", "0xe6ad5A88f5da0F276C903d9Ac2647A937c917162", "kChad Multisig"], + ["YEARN_MS", "mainnet", "0x16388463d60FFE0661Cf7F1f31a7D658aC790ff7", "Strategist Multisig (brain.ychad.eth)"], + ["YEARN_MS", "base-main", "0x01fE3347316b2223961B20689C65eaeA71348e93", "Strategist Multisig (base)"], + ["YEARN_MS", "katana-main", "0xBe7c7efc1ef3245d37E3157F76A512108D6D7aE6", "Strategist Multisig (katana)"], + ["YEARN_MS", "mainnet", "0x846e211e8ba920B353FB717631C015cf04061Cc9", "Core Dev Multisig (dev.ychad.eth)"], + ["YEARN_MS", "mainnet", "0xe5e2Baf96198c56380dDD5E992D7d1ADa0e989c0", "SAM Multisig (mainnet)"], + ["YEARN_MS", "base-main", "0xFEaE2F855250c36A77b8C68dB07C4dD9711fE36F", "SAM Multisig (base)"], + ["YEARN_MS", "katana-main", "0x518C21DC88D9780c0A1Be566433c571461A70149", "SAM Multisig (katana)"], + ["YEARN_MS", "mainnet", "0x90D0f26025571295D18a6c041E47450B81886B51", "Curation Multisig (mainnet)"], + ["YEARN_MS", "base-main", "0x90D0f26025571295D18a6c041E47450B81886B51", "Curation Multisig (base)"], + ["YEARN_MS", "katana-main", "0x90D0f26025571295D18a6c041E47450B81886B51", "Curation Multisig (katana)"], +] + +ALL_SAFE_ADDRESSES += YEARN_MULTISIGS + +# Yearn's bots/EOAs that routinely propose txs on each safe. Pending txs proposed +# by these addresses are skipped to cut noise — only "unexpected" proposers alert. +# Discovered from the last ~50 executed txs per safe (>=20% share, >=5 occurrences). +# Key: (network_name, safe_address_lower). Values: lowercase proposer addresses. +# Safes absent from this map are NOT filtered (alert on every tx). +YEARN_EXPECTED_PROPOSERS: dict[tuple[str, str], set[str]] = { + # yChad + ("mainnet", "0xfeb4acf3df3cdea7399794d0869ef76a6efaff52"): { + "0x962228a90eac69238c7d1f216d80037e61ea9255", + }, + # bChad + ("base-main", "0xbfaaba9f56a39b814281d68d2ad949e88d06b02e"): { + "0x623d4a04e19328244924d1dee48252987c02fc0a", + "0x5fcdc32dfc361a32e9d5ab9a384b890c62d0b8ac", + }, + # kChad + ("katana-main", "0xe6ad5a88f5da0f276c903d9ac2647a937c917162"): { + "0x623d4a04e19328244924d1dee48252987c02fc0a", + "0xf53d1fb2eed22cf1e8f7e90da7f1cae88344065f", + }, + # Strategist (non-katana share one bot) + ("mainnet", "0x16388463d60ffe0661cf7f1f31a7d658ac790ff7"): { + "0xd0002c648cca8dee2f2b8d70d542ccde8ad6ec03", + }, + ("base-main", "0x01fe3347316b2223961b20689c65eaea71348e93"): { + "0xd0002c648cca8dee2f2b8d70d542ccde8ad6ec03", + }, + # Strategist katana uses a different bot + ("katana-main", "0xbe7c7efc1ef3245d37e3157f76a512108d6d7ae6"): { + "0x1b5f15dcb82d25f91c65b53cee151e8b9fbdd271", + }, + # SAM Multisig (same bot every chain) + ("mainnet", "0xe5e2baf96198c56380ddd5e992d7d1ada0e989c0"): { + "0x80a3887ba60f76acab48ee4aead0a71a0774a8b2", + }, + ("base-main", "0xfeae2f855250c36a77b8c68db07c4dd9711fe36f"): { + "0x80a3887ba60f76acab48ee4aead0a71a0774a8b2", + }, + ("katana-main", "0x518c21dc88d9780c0a1be566433c571461a70149"): { + "0x80a3887ba60f76acab48ee4aead0a71a0774a8b2", + }, + # Curation Multisig (same bot every chain) + ("mainnet", "0x90d0f26025571295d18a6c041e47450b81886b51"): { + "0x80a3887ba60f76acab48ee4aead0a71a0774a8b2", + }, + ("base-main", "0x90d0f26025571295d18a6c041e47450b81886b51"): { + "0x80a3887ba60f76acab48ee4aead0a71a0774a8b2", + }, + ("katana-main", "0x90d0f26025571295d18a6c041e47450b81886b51"): { + "0x80a3887ba60f76acab48ee4aead0a71a0774a8b2", + }, + # Core Dev Multisig: historic txs predate Safe's proposer field — no filter. +} diff --git a/safe/main.py b/safe/main.py index 4659f87..147185b 100644 --- a/safe/main.py +++ b/safe/main.py @@ -5,6 +5,13 @@ import requests from dotenv import load_dotenv +from safe.addresses import ( + ALL_SAFE_ADDRESSES, + PROXY_UPGRADE_SIGNATURES, + YEARN_EXPECTED_PROPOSERS, + safe_address_network_prefix, + safe_apis, +) from safe.multisend import build_context_note, extract_inner_calls, safe_utility_label from safe.specific import handle_pendle from utils.cache import ( @@ -29,130 +36,6 @@ raise ValueError("At least one SAFE_API_KEY must be set.") _api_key_cycle = itertools.cycle(_api_keys) -safe_address_network_prefix = { - "mainnet": "eth", - "arbitrum-main": "arb1", - "optimism-main": "oeth", - "polygon-main": "matic", - "optim-yearn": "oeth", - "base-main": "base", -} - -safe_apis = { - "mainnet": "https://api.safe.global/tx-service/eth", - "arbitrum-main": "https://api.safe.global/tx-service/arb1", - "optimism-main": "https://api.safe.global/tx-service/oeth", - "polygon-main": "https://api.safe.global/tx-service/pol", - "base-main": "https://api.safe.global/tx-service/base", - # "optim-yearn": "https://safe-transaction-optimism.safe.global", -} - -PROXY_UPGRADE_SIGNATURES = [ - # Standard Proxy (OpenZeppelin, UUPS, Transparent) - "3659cfe6", # bytes4(keccak256("upgradeTo(address)")) - "4f1ef286", # upgradeToAndCall(address,bytes) - "f2fde38b", # changeProxyAdmin(address,address) - # Diamond Proxy (EIP-2535) - "1f931c1c", # diamondCut((address,uint8,bytes4[])[],address,bytes) -] - -# combined addresses, add more addresses if needed, last item is optional for additional info message -ALL_SAFE_ADDRESSES = [ - [ - "LIDO", - "mainnet", - "0x73b047fe6337183A454c5217241D780a932777bD", - ], # https://docs.lido.fi/multisigs/emergency-brakes/#12-emergency-brakes-ethereum - [ - "LIDO", - "mainnet", - "0x8772E3a2D86B9347A2688f9bc1808A6d8917760C", - ], # https://docs.lido.fi/multisigs/emergency-brakes/#11-gateseal-committee -> expires on 1 April 2025. - ["PENDLE", "mainnet", "0x8119EC16F0573B7dAc7C0CB94EB504FB32456ee1"], - ["PENDLE", "arbitrum-main", "0x7877AdFaDEd756f3248a0EBfe8Ac2E2eF87b75Ac"], - ["EULER", "mainnet", "0xcAD001c30E96765aC90307669d578219D4fb1DCe"], - [ - "AAVE", - "mainnet", - "0x2CFe3ec4d5a6811f4B8067F0DE7e47DfA938Aa30", - ], # aave Protocol Guardian Safe: https://app.aave.com/governance/v3/proposal/?proposalId=184 - ["AAVE", "polygon-main", "0xCb45E82419baeBCC9bA8b1e5c7858e48A3B26Ea6"], - ["AAVE", "arbitrum-main", "0xCb45E82419baeBCC9bA8b1e5c7858e48A3B26Ea6"], - [ - "AAVE", - "mainnet", - "0xCe52ab41C40575B072A18C9700091Ccbe4A06710", - ], # aave Governance Guardian Safe - ["AAVE", "polygon-main", "0x1A0581dd5C7C3DA4Ba1CDa7e0BcA7286afc4973b"], - ["AAVE", "arbitrum-main", "0x1A0581dd5C7C3DA4Ba1CDa7e0BcA7286afc4973b"], - [ - "MORPHO", - "mainnet", - "0x84258B3C495d8e9b10D0d4A7867392F149Da4274", - "Morpho eUSDe predeposit vault owner", - ], # eUSDe predeposit vault owner, token used by DAI vault on morpho - [ - "LRT", - "mainnet", - "0xb7cB7131FFc18f87eEc66991BECD18f2FF70d2af", - "LBTC boring vault big boss", - ], # LBTC boring vault big boss - # [ - # "LRT", - # "base-main", - # "0x92A19381444A001d62cE67BaFF066fA1111d7202", - # "Origin admin multisig. Markets used on Base", - # ], # origin admin - [ - "LRT", - "mainnet", - "0x9F6e831c8F8939DC0C830C6e492e7cEf4f9C2F5f", - "tBTC bridge owner multisig. aka, Council Multisig", - ], # tBTC bridge owner multisig (Council Multisig) - [ - "USDAI", - "arbitrum-main", - "0xF223F8d92465CfC303B3395fA3A25bfaE02AED51", - "USDai Admin Safe", - ], - [ - "USDAI", - "arbitrum-main", - "0x783B08aA21DE056717173f72E04Be0E91328A07b", - "sUSDai Admin Safe", - ], - [ - "CAP MONEY", - "mainnet", - "0xb8FC49402dF3ee4f8587268FB89fda4d621a8793", - "Cap Money Multisig", - ], - [ - "MAPLE", - "mainnet", - "0xd6d4Bcde6c816F17889f1Dd3000aF0261B03a196", - "Maple DAO Multisig (syrupUSDC)", - ], - [ - "STRATA", - "mainnet", - "0xA27cA9292268ee0f0258B749f1D5740c9Bb68B50", - "Strata Admin Multisig (3/4)", - ], - # [ - # "INFINIFI", - # "mainnet", - # "0x80608f852D152024c0a2087b16939235fEc2400c", - # "Infinifi Team Multisig", - # ], - # no active stargate strategies - # ["STARGATE", "mainnet", "0x65bb797c2B9830d891D87288F029ed8dACc19705"], - # ["STARGATE", "polygon-main", "0x47290DE56E71DC6f46C26e50776fe86cc8b21656"], - # ["STARGATE", "optimism-main", "0x392AC17A9028515a3bFA6CCe51F8b70306C6bd43"], - # ["STARGATE", "arbitrum-main", "0x9CD50907aeb5D16F29Bddf7e1aBb10018Ee8717d"], - # TEST: yearn ms in mainnet 0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52 -] - def get_safe_transactions( safe_address: str, network_name: str, executed: bool | None = None, limit: int = 10, max_retries: int = 3 @@ -274,6 +157,7 @@ def _explain_safe_tx( def check_for_pending_transactions(safe_address: str, network_name: str, protocol: str) -> None: pending_transactions = get_pending_transactions(safe_address, network_name) + expected_proposers = YEARN_EXPECTED_PROPOSERS.get((network_name, safe_address.lower())) if pending_transactions: for tx in pending_transactions: @@ -285,6 +169,18 @@ def check_for_pending_transactions(safe_address: str, network_name: str, protoco # send message for txs that target only vaults that we use in our strategies continue + if expected_proposers: + tx_proposer = (tx.get("proposer") or "").lower() + if tx_proposer in expected_proposers: + logger.info( + "Skipping nonce %s on %s — proposed by expected address %s", + nonce, + safe_address, + tx_proposer, + ) + write_last_executed_nonce_to_file(safe_address, nonce) + continue + message = ( "🚨 QUEUED TX DETECTED 🚨\n" f"🅿️ Protocol: {protocol}\n" diff --git a/utils/chains.py b/utils/chains.py index c25423f..0fc0821 100644 --- a/utils/chains.py +++ b/utils/chains.py @@ -51,6 +51,7 @@ def from_name(cls, name: str) -> "Chain": "polygon-main": "polygon", "base-main": "base", "optim-yearn": "optimism", + "katana-main": "katana", }