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
1 change: 1 addition & 0 deletions .github/workflows/_run-monitoring.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion morpho/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
201 changes: 201 additions & 0 deletions safe/addresses.py
Original file line number Diff line number Diff line change
@@ -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.
}
144 changes: 20 additions & 124 deletions safe/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions utils/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def from_name(cls, name: str) -> "Chain":
"polygon-main": "polygon",
"base-main": "base",
"optim-yearn": "optimism",
"katana-main": "katana",
}


Expand Down