From d7f7fdfde590aa346d766777389c77cedeb92783 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Mon, 5 Oct 2026 22:50:19 +0200 Subject: [PATCH 1/2] test: remove live-network, duplicate, and config-mirroring tests - Delete tests/test_tally.py (live HTTP/DNS calls against api.tally.xyz) - Drop timelock key tests already covered by test_alert_protocol_keys.py - Drop get_contract_label tests duplicating test_address_resolver.py - Replace weak KNOWN_SELECTORS sanity checks with a keccak integrity check - Read LLM default models from _PROVIDER_DEFAULTS instead of hardcoding them - Drop registry/config assertions that restate pegged assets, Morpho, timelocks - Merge per-severity alert emoji/silence and dispatch skip tests - Remove unused utils/tenderly/alerts_with_timelock.json Co-Authored-By: Claude Opus 5.5 --- tests/test_alert.py | 102 +-- tests/test_calldata_decoder.py | 18 +- tests/test_dispatch.py | 14 +- tests/test_llm.py | 44 +- tests/test_morpho_markets.py | 5 - tests/test_pegged_assets.py | 22 - tests/test_source_context.py | 42 -- tests/test_tally.py | 187 ----- tests/test_timelock_alerts.py | 33 - utils/tenderly/alerts_with_timelock.json | 851 ----------------------- 10 files changed, 42 insertions(+), 1276 deletions(-) delete mode 100644 tests/test_tally.py delete mode 100644 utils/tenderly/alerts_with_timelock.json diff --git a/tests/test_alert.py b/tests/test_alert.py index 41d4ec9d..417f05a8 100644 --- a/tests/test_alert.py +++ b/tests/test_alert.py @@ -9,12 +9,6 @@ class TestAlert(unittest.TestCase): """Tests for the Alert system.""" - def test_severity_enum_values(self): - self.assertEqual(AlertSeverity.LOW.value, "LOW") - self.assertEqual(AlertSeverity.MEDIUM.value, "MEDIUM") - self.assertEqual(AlertSeverity.HIGH.value, "HIGH") - self.assertEqual(AlertSeverity.CRITICAL.value, "CRITICAL") - def test_alert_dataclass_immutability(self): alert = Alert(severity=AlertSeverity.HIGH, message="test", protocol="proto") with self.assertRaises(AttributeError): @@ -22,80 +16,28 @@ def test_alert_dataclass_immutability(self): alert.message = "changed" # ty: ignore[invalid-assignment] @patch("utils.alert.send_telegram_message") - def test_emoji_prefix_low(self, mock_send): - alert = Alert(severity=AlertSeverity.LOW, message="info msg", protocol="test") - send_alert(alert) - mock_send.assert_called_once_with( - "ā„¹ļø info msg", - "test", - True, - False, - severity="LOW", - source="protocol", - origin_protocol="test", - channel="test", - ) - - @patch("utils.alert.send_telegram_message") - def test_emoji_prefix_medium(self, mock_send): - alert = Alert(severity=AlertSeverity.MEDIUM, message="warn msg", protocol="test") - send_alert(alert) - mock_send.assert_called_once_with( - "āš ļø warn msg", - "test", - False, - False, - severity="MEDIUM", - source="protocol", - origin_protocol="test", - channel="test", - ) - - @patch("utils.alert.send_telegram_message") - def test_emoji_prefix_high(self, mock_send): - alert = Alert(severity=AlertSeverity.HIGH, message="high msg", protocol="test") - send_alert(alert) - mock_send.assert_called_once_with( - "🚨 high msg", - "test", - False, - False, - severity="HIGH", - source="protocol", - origin_protocol="test", - channel="test", - ) - - @patch("utils.alert.send_telegram_message") - def test_emoji_prefix_critical(self, mock_send): - alert = Alert(severity=AlertSeverity.CRITICAL, message="crit msg", protocol="test") - send_alert(alert) - mock_send.assert_called_once_with( - "šŸ”“ crit msg", - "test", - False, - False, - severity="CRITICAL", - source="protocol", - origin_protocol="test", - channel="test", - ) - - @patch("utils.alert.send_telegram_message") - def test_silent_default_low(self, mock_send): - # LOW defaults to silent=True - send_alert(Alert(severity=AlertSeverity.LOW, message="m", protocol="p")) - _, args, _ = mock_send.mock_calls[0] - self.assertTrue(args[2], "LOW should default to silent") - - @patch("utils.alert.send_telegram_message") - def test_silent_default_medium_high_critical(self, mock_send): - # MEDIUM, HIGH and CRITICAL default to silent=False (loud) - for sev in (AlertSeverity.MEDIUM, AlertSeverity.HIGH, AlertSeverity.CRITICAL): - mock_send.reset_mock() - send_alert(Alert(severity=sev, message="m", protocol="p")) - _, args, _ = mock_send.mock_calls[0] - self.assertFalse(args[2], f"{sev.value} should default to loud") + def test_emoji_prefix_and_default_silence(self, mock_send): + # LOW is silent by default; MEDIUM and above notify. + cases = [ + (AlertSeverity.LOW, "ā„¹ļø", True), + (AlertSeverity.MEDIUM, "āš ļø", False), + (AlertSeverity.HIGH, "🚨", False), + (AlertSeverity.CRITICAL, "šŸ”“", False), + ] + for severity, emoji, silent in cases: + with self.subTest(severity=severity.value): + mock_send.reset_mock() + send_alert(Alert(severity=severity, message="msg", protocol="test")) + mock_send.assert_called_once_with( + f"{emoji} msg", + "test", + silent, + False, + severity=severity.value, + source="protocol", + origin_protocol="test", + channel="test", + ) @patch("utils.alert.send_telegram_message") def test_silent_explicit_override(self, mock_send): diff --git a/tests/test_calldata_decoder.py b/tests/test_calldata_decoder.py index a7c772f4..8de407ae 100644 --- a/tests/test_calldata_decoder.py +++ b/tests/test_calldata_decoder.py @@ -3,7 +3,7 @@ import unittest from unittest.mock import MagicMock, patch -from eth_utils import to_checksum_address +from eth_utils import function_signature_to_4byte_selector, to_checksum_address from utils.calldata.decoder import ( DecodedCall, @@ -213,20 +213,12 @@ def test_no_match(self, mock_fetch): class TestKnownSelectors(unittest.TestCase): - """Sanity checks for the known_selectors table.""" + """Integrity check for the known_selectors table.""" - def test_table_not_empty(self): - self.assertGreater(len(KNOWN_SELECTORS), 0) - - def test_all_selectors_are_valid_hex(self): - for selector in KNOWN_SELECTORS: - self.assertTrue(selector.startswith("0x"), f"Missing 0x prefix: {selector}") - self.assertEqual(len(selector), 10, f"Wrong length: {selector}") - - def test_all_signatures_have_parens(self): + def test_selectors_match_signatures(self): + self.assertTrue(KNOWN_SELECTORS) for selector, sig in KNOWN_SELECTORS.items(): - self.assertIn("(", sig, f"No opening paren in {selector}: {sig}") - self.assertIn(")", sig, f"No closing paren in {selector}: {sig}") + self.assertEqual(selector, "0x" + function_signature_to_4byte_selector(sig).hex(), sig) # transfer(address,uint256) calldata: diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py index e3bd0d10..80546281 100644 --- a/tests/test_dispatch.py +++ b/tests/test_dispatch.py @@ -96,19 +96,11 @@ def test_dispatch_uses_configured_webhook_url(self, mock_cooldown, mock_record, self.assertEqual(mock_post.call_args[0][0], "http://localhost:9000/webhook/emergency") @patch("utils.dispatch.requests.post") - def test_dispatch_skips_low_severity(self, mock_post): + def test_dispatch_skips_low_and_medium_severity(self, mock_post): from utils.dispatch import dispatch_emergency_withdrawal - alert = Alert(severity=AlertSeverity.LOW, message="info", protocol="infinifi") - dispatch_emergency_withdrawal(alert) - mock_post.assert_not_called() - - @patch("utils.dispatch.requests.post") - def test_dispatch_skips_medium_severity(self, mock_post): - from utils.dispatch import dispatch_emergency_withdrawal - - alert = Alert(severity=AlertSeverity.MEDIUM, message="warn", protocol="infinifi") - dispatch_emergency_withdrawal(alert) + for severity in (AlertSeverity.LOW, AlertSeverity.MEDIUM): + dispatch_emergency_withdrawal(Alert(severity=severity, message="m", protocol="infinifi")) mock_post.assert_not_called() @patch("utils.dispatch.requests.post") diff --git a/tests/test_llm.py b/tests/test_llm.py index 4568a20b..f1ad0a27 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -199,13 +199,6 @@ def test_model_name_property(self, mock_anthropic_cls: MagicMock) -> None: provider = AnthropicProvider(api_key="key", model="claude-haiku-4-5-20251001") self.assertEqual(provider.model_name, "claude-haiku-4-5-20251001") - @patch("anthropic.Anthropic") - def test_structured_output_enabled_by_default(self, mock_anthropic_cls: MagicMock) -> None: - from utils.llm.anthropic_provider import AnthropicProvider - - provider = AnthropicProvider(api_key="key", model="claude-haiku-4-5-20251001") - self.assertTrue(provider.supports_structured_output) - @patch("anthropic.Anthropic") def test_complete_structured_uses_forced_tool(self, mock_anthropic_cls: MagicMock) -> None: from utils.llm.anthropic_provider import AnthropicProvider @@ -244,33 +237,20 @@ def test_missing_api_key_raises(self) -> None: get_llm_provider() self.assertIn("LLM_API_KEY", str(ctx.exception)) - @patch("openai.OpenAI") - def test_venice_defaults(self, mock_openai_cls: MagicMock) -> None: - env = {"LLM_PROVIDER": "venice", "LLM_API_KEY": "test-key"} - with patch.dict(os.environ, env, clear=True): - provider = get_llm_provider() - self.assertEqual(provider.model_name, "deepseek-v4-flash") - - @patch("openai.OpenAI") - def test_openai_defaults(self, mock_openai_cls: MagicMock) -> None: - env = {"LLM_PROVIDER": "openai", "LLM_API_KEY": "sk-test"} - with patch.dict(os.environ, env, clear=True): - provider = get_llm_provider() - self.assertEqual(provider.model_name, "gpt-4o-mini") - @patch("anthropic.Anthropic") - def test_anthropic_defaults(self, mock_anthropic_cls: MagicMock) -> None: - env = {"LLM_PROVIDER": "anthropic", "LLM_API_KEY": "sk-ant-test"} - with patch.dict(os.environ, env, clear=True): - provider = get_llm_provider() - self.assertEqual(provider.model_name, "claude-haiku-4-5-20251001") + @patch("openai.OpenAI") + def test_provider_uses_default_model(self, mock_openai_cls: MagicMock, mock_anthropic_cls: MagicMock) -> None: + from utils.llm.anthropic_provider import AnthropicProvider - @patch("anthropic.Anthropic") - def test_anthropic_custom_model(self, mock_anthropic_cls: MagicMock) -> None: - env = {"LLM_PROVIDER": "anthropic", "LLM_API_KEY": "sk-ant-test", "LLM_MODEL": "claude-sonnet-4-6"} - with patch.dict(os.environ, env, clear=True): - provider = get_llm_provider() - self.assertEqual(provider.model_name, "claude-sonnet-4-6") + for name in ("venice", "openai", "anthropic"): + with ( + self.subTest(provider=name), + patch.dict(os.environ, {"LLM_PROVIDER": name, "LLM_API_KEY": "k"}, clear=True), + ): + reset_provider() + provider = get_llm_provider() + self.assertEqual(provider.model_name, _PROVIDER_DEFAULTS[name]["model"]) + self.assertEqual(isinstance(provider, AnthropicProvider), name == "anthropic") @patch("openai.OpenAI") def test_custom_overrides(self, mock_openai_cls: MagicMock) -> None: diff --git a/tests/test_morpho_markets.py b/tests/test_morpho_markets.py index 93c03156..6af93936 100644 --- a/tests/test_morpho_markets.py +++ b/tests/test_morpho_markets.py @@ -99,11 +99,6 @@ def test_discovery_skips_vaults_without_market_monitoring(self) -> None: self.assertEqual(discover_v2_vaults_by_chain(), {}) request.assert_not_called() - def test_hyperevm_ousd_v2_wrapper_is_governance_only(self) -> None: - (wrapper,) = VAULTS_V2_BY_CHAIN[Chain.HYPEREVM] - self.assertEqual(wrapper.address, "0xE90959cbE7E56b5eBFF9AD12de611A4976F2d2B1") - self.assertFalse(wrapper.monitor_markets) - def test_discovery_rejects_non_market_adapters(self) -> None: item = { "adapters": { diff --git a/tests/test_pegged_assets.py b/tests/test_pegged_assets.py index d17d08cb..c4bd135a 100644 --- a/tests/test_pegged_assets.py +++ b/tests/test_pegged_assets.py @@ -94,13 +94,6 @@ def test_names_are_unique(self): names = [a.name for a in PEGGED_ASSETS] self.assertEqual(len(names), len(set(names))) - def test_address_parsed_from_defillama_key(self): - self.assertEqual(get_asset("USDC").address, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") - - def test_btc_pegged_assets_target_btc(self): - self.assertEqual(get_asset("cbBTC").peg, PegTarget.BTC) - self.assertEqual(get_asset("LBTC").peg, PegTarget.BTC) - def test_get_asset_unknown_raises(self): with self.assertRaises(KeyError): get_asset("NOPE") @@ -113,21 +106,6 @@ def test_btc_wrappers_are_downside_only(self): for name in ("WBTC", "cbBTC", "LBTC"): self.assertTrue(get_asset(name).downside_only, name) - def test_usd_stables_are_symmetric(self): - self.assertFalse(get_asset("USDC").downside_only) - - def test_chainlink_feed_quote_denomination(self): - # BTC-denominated feeds (answer ~1.0) vs USD-denominated feeds (absolute price). - for name, quote in ( - ("WBTC", PegTarget.BTC), - ("LBTC", PegTarget.BTC), - ("cbBTC", PegTarget.USD), - ("USDC", PegTarget.USD), - ): - feed = get_asset(name).chainlink_feed - assert feed is not None - self.assertEqual(feed.quote, quote) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_source_context.py b/tests/test_source_context.py index 18f25dab..576389cd 100644 --- a/tests/test_source_context.py +++ b/tests/test_source_context.py @@ -434,11 +434,6 @@ def test_unverified_returns_empty(self, mock_fetch: MagicMock) -> None: label = get_contract_label(1, "0xac21b22b5aeb11bc32de4ecf59e4538fca48b694") self.assertEqual(label, "") - def test_safe_utility_shortcut(self) -> None: - # MultiSendCallOnly — no Etherscan call should be needed. - label = get_contract_label(1, "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D") - self.assertEqual(label, "Safe MultiSendCallOnly") - @patch.dict("os.environ", {"ETHERSCAN_TOKEN": "test-key"}) @patch("utils.proxy.get_current_implementation") @patch("utils.source_context.fetch_json") @@ -463,43 +458,6 @@ def test_keeps_specific_name_without_proxy_follow(self, mock_fetch: MagicMock, m self.assertEqual(label, "FarmRegistry") mock_impl.assert_not_called() - def test_empty_address_returns_empty(self) -> None: - self.assertEqual(get_contract_label(1, ""), "") - - @patch.dict("os.environ", {"ETHERSCAN_TOKEN": "test-key"}) - @patch("utils.swiss_knife.fetch_json") - @patch("utils.source_context.fetch_json") - def test_prefers_swiss_knife_over_etherscan(self, mock_es: MagicMock, mock_sk: MagicMock) -> None: - # Swiss Knife knows USDC by its full curated name; Etherscan would just - # return "FiatTokenV2_2". We want the curated label. - from utils.swiss_knife import reset_cache as sk_reset - - sk_reset() - mock_sk.return_value = ["Circle: USDC Token", "circle", "stablecoin"] - mock_es.return_value = { - "status": "1", - "result": [{"SourceCode": "/* x */", "ContractName": "FiatTokenV2_2"}], - } - label = get_contract_label(1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") - self.assertEqual(label, "Circle: USDC Token") - # Etherscan should not have been hit since Swiss Knife was authoritative. - mock_es.assert_not_called() - - @patch.dict("os.environ", {"ETHERSCAN_TOKEN": "test-key"}) - @patch("utils.swiss_knife.fetch_json") - @patch("utils.source_context.fetch_json") - def test_falls_back_to_etherscan_when_swiss_knife_empty(self, mock_es: MagicMock, mock_sk: MagicMock) -> None: - from utils.swiss_knife import reset_cache as sk_reset - - sk_reset() - mock_sk.return_value = {"error": "Error fetching data"} - mock_es.return_value = { - "status": "1", - "result": [{"SourceCode": "/* x */", "ContractName": "FarmRegistry"}], - } - label = get_contract_label(1, "0xac21b22b5aeb11bc32de4ecf59e4538fca48b694") - self.assertEqual(label, "FarmRegistry") - class TestFetchFunctionInputNames(unittest.TestCase): """ABI-driven parameter name extraction.""" diff --git a/tests/test_tally.py b/tests/test_tally.py deleted file mode 100644 index 3b8adbe9..00000000 --- a/tests/test_tally.py +++ /dev/null @@ -1,187 +0,0 @@ -import os -import socket -import time - -import pytest -import requests -from dotenv import load_dotenv - -load_dotenv() - -_REQUIRES_TALLY_KEY = pytest.mark.skipif(not os.getenv("TALLY_API_KEY"), reason="TALLY_API_KEY not set in environment") - - -def _check_dns_resolution() -> bool: - """Check if we can resolve the Tally API hostname.""" - try: - ip = socket.gethostbyname("api.tally.xyz") - print(f"DNS resolution successful: api.tally.xyz -> {ip}") - return True - except socket.gaierror as e: - print(f"DNS resolution failed: {e}") - return False - - -def test_dns_resolution(): - """Test if we can resolve the Tally API hostname.""" - assert _check_dns_resolution() - - -def _check_basic_connectivity() -> bool: - """Check basic HTTP connectivity without API key.""" - try: - print("Testing basic connectivity to api.tally.xyz...") - response = requests.get("https://api.tally.xyz", timeout=10) - print(f"Basic connection status: {response.status_code}") - print(f"Response headers: {dict(response.headers)}") - return True - except requests.exceptions.Timeout: - print("Basic connectivity timeout") - return False - except requests.exceptions.ConnectionError as e: - print(f"Connection error: {e}") - return False - except Exception as e: - print(f"Basic connectivity test failed: {e}") - return False - - -def test_basic_connectivity(): - """Test basic HTTP connectivity without API key.""" - assert _check_basic_connectivity() - - -def _check_with_different_timeouts() -> bool: - """Check API with progressively longer timeouts.""" - api_key = os.getenv("TALLY_API_KEY") - if not api_key: - print("TALLY_API_KEY not found in environment") - return False - - headers = {"Api-Key": api_key, "Content-Type": "application/json"} - - # Very simple query - query = """ - query { - chains { - id - } - } - """ - - timeouts = [5, 15, 30, 60] - - for timeout in timeouts: - print(f"\nTrying with {timeout}s timeout...") - try: - start_time = time.time() - response = requests.post( - "https://api.tally.xyz/query", - json={"query": query}, - headers=headers, - timeout=timeout, - ) - - elapsed = time.time() - start_time - print(f"Response received in {elapsed:.2f}s") - print(f"Status: {response.status_code}") - - if response.status_code == 200: - print("Success!") - return True - else: - print(f"Non-200 status: {response.text[:200]}") - - except requests.exceptions.Timeout: - print(f"Timeout after {timeout}s") - continue - except Exception as e: - print(f"Error with {timeout}s timeout: {e}") - continue - - return False - - -@_REQUIRES_TALLY_KEY -def test_with_different_timeouts(): - """Test API with progressively longer timeouts.""" - assert _check_with_different_timeouts() - - -def _check_alternative_endpoints() -> bool: - """Check if there are alternative endpoints or if the main one is down.""" - api_key = os.getenv("TALLY_API_KEY") - if not api_key: - print("TALLY_API_KEY not found") - return False - - # Test if we can reach the main Tally website - try: - print("Testing main Tally website...") - response = requests.get("https://www.tally.xyz", timeout=10) - print(f"Tally website status: {response.status_code}") - except Exception as e: - print(f"Tally website unreachable: {e}") - - return True - - -@_REQUIRES_TALLY_KEY -def test_alternative_endpoints(): - """Test if there are alternative endpoints or if the main one is down.""" - assert _check_alternative_endpoints() - - -def test_network_with_curl(): - """Print curl command for manual testing""" - api_key = os.getenv("TALLY_API_KEY") - if api_key: - curl_cmd = f"""curl -X POST https://api.tally.xyz/query \\ - -H "Api-Key: {api_key}" \\ - -H "Content-Type: application/json" \\ - -d '{{"query": "query {{ chains {{ id name }} }}"}}' \\ - --max-time 30""" - - print("\nTest manually with curl:") - print(curl_cmd) - - -def main(): - print("=== Tally API Connectivity Diagnosis ===\n") - - # Step 1: DNS resolution - print("1. Testing DNS resolution...") - if not _check_dns_resolution(): - print("āŒ DNS resolution failed - check your internet connection") - return - - # Step 2: Basic connectivity - print("\n2. Testing basic connectivity...") - if not _check_basic_connectivity(): - print("āŒ Basic connectivity failed - possible firewall/proxy issues") - return - - # Step 3: API endpoint with different timeouts - print("\n3. Testing API endpoint with different timeouts...") - if _check_with_different_timeouts(): - print("āœ… API connection successful!") - return - - # Step 4: Check alternatives - print("\n4. Testing alternative endpoints...") - _check_alternative_endpoints() - - # Step 5: Manual test command - print("\n5. Manual test option:") - test_network_with_curl() - - print("\nāŒ All automatic tests failed. Possible causes:") - print("- Tally API is down/experiencing issues") - print("- Your network/firewall is blocking the connection") - print("- ISP issues with HTTPS connections to api.tally.xyz") - print("- Try the curl command above manually") - print("- Check Tally's status page or Discord for outages") - - -if __name__ == "__main__": - main() diff --git a/tests/test_timelock_alerts.py b/tests/test_timelock_alerts.py index c0000ec4..aad6541a 100644 --- a/tests/test_timelock_alerts.py +++ b/tests/test_timelock_alerts.py @@ -5,44 +5,11 @@ from unittest.mock import MagicMock, patch from protocols.timelock.timelock_alerts import ( - TIMELOCKS, - YEARN_TIMELOCK_INTERNAL_PROTOCOL, TimelockConfig, - alert_history_protocol, build_alert_message, - process_events, ) from utils.telegram import MAX_MESSAGE_LENGTH -YEARN_TIMELOCK_ADDRESS = "0x88ba032be87d5ef1fbe87336b7090767f367bf73" - - -def test_3jane_seven_day_timelock_is_monitored() -> None: - timelock = TIMELOCKS[("0x3d3c41419ab401cd25055e8f9421d7d96d887885", 1)] - - assert timelock.protocol == "3JANE" - assert timelock.label == "3Jane 7d TimelockController" - - -def test_alert_history_protocol_matches_website_keys() -> None: - assert alert_history_protocol("YEARN_TIMELOCK") == "yearn" - assert alert_history_protocol("RTOKEN") == "ethplus" - assert alert_history_protocol("COMP") == "comp" - assert alert_history_protocol("3JANE") == "3jane" - assert alert_history_protocol("CAP") == "cap" - - -@patch("protocols.timelock.timelock_alerts.send_telegram_message") -@patch("protocols.timelock.timelock_alerts.build_alert_message", return_value="msg") -def test_yearn_timelock_alert_recorded_under_yearn(_mock_build: object, mock_send: unittest.mock.MagicMock) -> None: - """Routing stays on YEARN_TIMELOCK, but the stored protocol must be `yearn` for the website.""" - event = _make_event(id="1", timelockAddress=YEARN_TIMELOCK_ADDRESS) - - process_events([event], use_cache=False) - - calls = {c.args[1]: c.kwargs["origin_protocol"] for c in mock_send.call_args_list} - assert calls == {"YEARN_TIMELOCK": "yearn", YEARN_TIMELOCK_INTERNAL_PROTOCOL: None} - def _make_event( timelock_type: str = "TimelockController", diff --git a/utils/tenderly/alerts_with_timelock.json b/utils/tenderly/alerts_with_timelock.json deleted file mode 100644 index 3d308ae9..00000000 --- a/utils/tenderly/alerts_with_timelock.json +++ /dev/null @@ -1,851 +0,0 @@ -{ - "alerts": [ - { - "id": "12da72da-b69c-40c6-862a-9d88538be13c", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in superOETH Timelock", - "description": "superOETH is LRT by Origin Protocol", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-08-04T13:26:27.330817Z", - "delivery_channels": [ - { - "delivery_channel_id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "delivery_channel": { - "id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Renzo Alerts SAM", - "reference_id": "38bb1f0c-bf15-40a0-9117-9917679d37da", - "enabled": true, - "created_at": "2024-10-13T21:45:04.627275Z", - "information": { - "chat_id": -4591087023 - } - }, - "enabled": true, - "created_at": "2025-08-04T13:30:46.510461Z" - } - ], - "is_editable": true, - "updated_at": "2025-08-04T13:30:46.513231Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0xf817cb3092179083c48c014688d98b72fb61464f", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "8453" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0xf817cb3092179083c48c014688d98b72fb61464f", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "14fde22a-110d-43ff-be49-6f4b77ed3ecb", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted from ETH+", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-07-02T16:46:01.594393Z", - "delivery_channels": [ - { - "delivery_channel_id": "7d4d7962-6af0-4bab-9cf3-5526df755283", - "delivery_channel": { - "id": "7d4d7962-6af0-4bab-9cf3-5526df755283", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "ETH+ Alerts SAM", - "reference_id": "5690897e-e918-445f-958f-dd646ee448da", - "enabled": true, - "created_at": "2025-04-25T19:41:06.669325Z", - "information": { - "chat_id": -4745822324 - } - }, - "enabled": true, - "created_at": "2025-07-02T16:47:02.464452Z" - } - ], - "is_editable": true, - "updated_at": "2025-07-02T16:47:02.466331Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x5d8a7dc9405f08f14541ba918c1bf7eb2dace556", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0x5d8a7dc9405f08f14541ba918c1bf7eb2dace556", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "271040e6-85bc-4103-bf05-094a9912961a", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in LombardTimeLock", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-06-19T09:39:04.203509Z", - "delivery_channels": [ - { - "delivery_channel_id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "delivery_channel": { - "id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Renzo Alerts SAM", - "reference_id": "38bb1f0c-bf15-40a0-9117-9917679d37da", - "enabled": true, - "created_at": "2024-10-13T21:45:04.627275Z", - "information": { - "chat_id": -4591087023 - } - }, - "enabled": true, - "created_at": "2025-06-19T09:39:21.252334Z" - } - ], - "is_editable": true, - "updated_at": "2025-06-19T09:39:21.254191Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x055e84e7fe8955e2781010b866f10ef6e1e77e59", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0x055e84e7fe8955e2781010b866f10ef6e1e77e59", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "287fff5c-a2aa-4320-9893-1957ef389c7c", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "thBILL oracle price changed > 1%", - "description": "Redstone thBILL_FUNDAMENTAL price changed on Arbitrum more than 1%: https://app.redstone.finance/app/token/thBILL_FUNDAMENTAL/", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-09-16T08:33:16.279198Z", - "delivery_channels": [ - { - "delivery_channel_id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "delivery_channel": { - "id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Renzo Alerts SAM", - "reference_id": "38bb1f0c-bf15-40a0-9117-9917679d37da", - "enabled": true, - "created_at": "2024-10-13T21:45:04.627275Z", - "information": { - "chat_id": -4591087023 - } - }, - "enabled": true, - "created_at": "2025-09-16T08:34:21.628288Z" - } - ], - "is_editable": true, - "updated_at": "2025-09-16T08:34:21.631893Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x8e66f244f05277aae16e1b5a586d742df932f157", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "42161" - } - }, - { - "type": "view_function", - "expression": { - "address": "0x8e66f244f05277aae16e1b5a586d742df932f157", - "input": "0xa035b1fe", - "parameter_condition": { - "parameter_name": "price", - "operator": ">=", - "parameter_type": "uint", - "nested_parameter_type": "", - "comparison_value": "1", - "compare_percentage": true - }, - "network_id": "42161" - } - } - ] - }, - { - "id": "316f440e-457b-4cfa-a69e-f7f54230bf44", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "cUSD fundamental oracle value is below threshold", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-12-24T23:17:27.439824Z", - "delivery_channels": [ - { - "delivery_channel_id": "acdd2d9a-70da-4805-b7a3-da00f6619d2c", - "delivery_channel": { - "id": "acdd2d9a-70da-4805-b7a3-da00f6619d2c", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "CAP Alerts SAM", - "reference_id": "d9fe061d-9db7-4290-a95f-b378c8047c89", - "enabled": true, - "created_at": "2025-12-19T01:38:35.286271Z", - "information": { - "chat_id": -1003568823334 - } - }, - "enabled": true, - "created_at": "2026-01-02T15:11:22.218309Z" - } - ], - "is_editable": true, - "updated_at": "2026-01-02T15:11:22.221554Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x9a5a3c3ed0361505cc1d4e824b3854de5724434a", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "view_function", - "expression": { - "address": "0x9a5a3c3ed0361505cc1d4e824b3854de5724434a", - "input": "0x50d25bcd", - "parameter_condition": { - "parameter_name": "latestAnswer", - "operator": "<", - "parameter_type": "int", - "nested_parameter_type": "", - "comparison_value": "99980000", - "compare_threshold": true - }, - "network_id": "1" - } - } - ] - }, - { - "id": "6c4b81c9-3130-4b3c-9356-15c9abc7b918", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Function plot called in Maker DAO Pause Proxy", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-08-23T09:50:53.141367Z", - "delivery_channels": [ - { - "delivery_channel_id": "d269e1fe-465a-432b-ab01-129f155eff83", - "delivery_channel": { - "id": "d269e1fe-465a-432b-ab01-129f155eff83", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "MakerDAO Alerts SAM", - "reference_id": "2e3af9b9-6814-4dcb-9a2e-a1d79e95ba0f", - "enabled": true, - "created_at": "2024-08-22T11:21:46.882552Z", - "information": { - "chat_id": -1002184015556 - } - }, - "enabled": true, - "created_at": "2024-09-02T12:16:18.121432Z" - } - ], - "is_editable": true, - "updated_at": "2024-09-02T12:16:18.12557Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0xbe286431454714f511008713973d3b053a2d38f3", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "method_call", - "expression": { - "address": "0xbe286431454714f511008713973d3b053a2d38f3", - "function_name": "plot", - "signature": { - "function_name": "plot", - "input_types": [ - "address", - "bytes32", - "bytes", - "uint256" - ] - } - } - } - ] - }, - { - "id": "8e577a18-92b2-4cab-86b8-53c7c3025a00", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event StartVote emitted in Lido DAO Voting", - "description": "Using event to cover both `newVote()` function", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-08-19T14:16:54.683101Z", - "delivery_channels": [ - { - "delivery_channel_id": "1e67aaa6-6f1d-4154-8072-b6b233d88705", - "delivery_channel": { - "id": "1e67aaa6-6f1d-4154-8072-b6b233d88705", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Lido Alerts SAM", - "reference_id": "bfe7315c-8394-4c79-96c9-90df8d157f48", - "enabled": true, - "created_at": "2024-08-07T23:10:59.132381Z", - "information": { - "chat_id": -1002163597367 - } - }, - "enabled": true, - "created_at": "2024-09-02T12:16:56.058401Z" - } - ], - "is_editable": true, - "updated_at": "2024-09-02T12:16:56.079856Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x2e59a20f205bb85a89c53f1936454680651e618e", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0x2e59a20f205bb85a89c53f1936454680651e618e", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "StartVote", - "event_id": "0x4d72fe0577a3a3f7da968d7b892779dde102519c25527b29cf7054f245c791b9", - "decode_events": false - } - } - ] - }, - { - "id": "9be6d06d-83a1-46be-bb1a-4bbaa813ef52", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in EtherFiTimelock", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-10-14T12:23:31.178374Z", - "delivery_channels": [ - { - "delivery_channel_id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "delivery_channel": { - "id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Renzo Alerts SAM", - "reference_id": "38bb1f0c-bf15-40a0-9117-9917679d37da", - "enabled": true, - "created_at": "2024-10-13T21:45:04.627275Z", - "information": { - "chat_id": -4591087023 - } - }, - "enabled": true, - "created_at": "2024-10-14T12:23:31.206846Z" - } - ], - "is_editable": true, - "updated_at": "2024-10-14T12:23:31.178374Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "a22678ba-dc69-4f22-86d9-18757a688d3d", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in CAP TimelockController", - "description": "Timelock controller is owner of cUSD vault", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-12-19T01:39:13.677282Z", - "delivery_channels": [ - { - "delivery_channel_id": "acdd2d9a-70da-4805-b7a3-da00f6619d2c", - "delivery_channel": { - "id": "acdd2d9a-70da-4805-b7a3-da00f6619d2c", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "CAP Alerts SAM", - "reference_id": "d9fe061d-9db7-4290-a95f-b378c8047c89", - "enabled": true, - "created_at": "2025-12-19T01:38:35.286271Z", - "information": { - "chat_id": -1003568823334 - } - }, - "enabled": true, - "created_at": "2025-12-19T01:39:55.846285Z" - } - ], - "is_editable": true, - "updated_at": "2025-12-19T01:39:55.848978Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0xd8236031d8279d82e615af2bfab5fc0127a329ab", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0xd8236031d8279d82e615af2bfab5fc0127a329ab", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "c8108fff-b1f4-4cb0-abd3-c37ad541e6aa", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in KelpDAO(rsETH) Timelock", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-10-14T12:34:15.870657Z", - "delivery_channels": [ - { - "delivery_channel_id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "delivery_channel": { - "id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Renzo Alerts SAM", - "reference_id": "38bb1f0c-bf15-40a0-9117-9917679d37da", - "enabled": true, - "created_at": "2024-10-13T21:45:04.627275Z", - "information": { - "chat_id": -4591087023 - } - }, - "enabled": true, - "created_at": "2025-03-13T12:25:52.372424Z" - } - ], - "is_editable": true, - "updated_at": "2025-03-13T12:25:52.374903Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x49bd9989e31ad35b0a62c20be86335196a3135b1", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0x49bd9989e31ad35b0a62c20be86335196a3135b1", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "e978ddbe-5631-424d-bead-54079286e5e5", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Function queueTransaction called in Compound Timelock", - "description": "Queues transactions for all chains", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-08-22T11:02:17.877258Z", - "delivery_channels": [ - { - "delivery_channel_id": "3e4e3c26-e9a3-4a8b-ade8-3c517d757bcf", - "delivery_channel": { - "id": "3e4e3c26-e9a3-4a8b-ade8-3c517d757bcf", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Compound Alerts SAM", - "reference_id": "f4571663-57bd-4cfd-9e26-362ae107fc16", - "enabled": true, - "created_at": "2024-08-22T11:00:31.754357Z", - "information": { - "chat_id": -1002214231139 - } - }, - "enabled": true, - "created_at": "2024-09-02T11:56:22.964936Z" - } - ], - "is_editable": true, - "updated_at": "2024-09-02T11:56:22.978939Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x6d903f6003cca6255d85cca4d3b5e5146dc33925", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "method_call", - "expression": { - "address": "0x6d903f6003cca6255d85cca4d3b5e5146dc33925", - "function_name": "queueTransaction", - "signature": { - "function_name": "queueTransaction", - "input_types": [ - "address", - "uint256", - "string", - "bytes", - "uint256" - ] - } - } - } - ] - }, - { - "id": "eae36bdd-b44c-45d5-acf7-b013238e32c0", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Function queueProposal called in AAVE Timelock", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-08-23T17:04:08.122822Z", - "delivery_channels": [ - { - "delivery_channel_id": "6fbcff60-bda5-492b-9a6d-658fbeb6f261", - "delivery_channel": { - "id": "6fbcff60-bda5-492b-9a6d-658fbeb6f261", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "AAVE Alerts SAM", - "reference_id": "65d97d0c-deda-430f-b5b5-65ee6270ea52", - "enabled": true, - "created_at": "2024-08-07T12:16:41.831658Z", - "information": { - "chat_id": -1002192964685 - } - }, - "enabled": true, - "created_at": "2024-09-02T12:15:07.465951Z" - } - ], - "is_editable": true, - "updated_at": "2024-09-02T12:15:07.475331Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x9aee0b04504cef83a65ac3f0e838d0593bcb2bc7", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "method_call", - "expression": { - "address": "0x9aee0b04504cef83a65ac3f0e838d0593bcb2bc7", - "function_name": "queueProposal", - "signature": { - "function_name": "queueProposal", - "input_types": [ - "uint256", - "uint128", - "uint128" - ] - } - } - } - ] - }, - { - "id": "eca272ef-979a-47b3-a7f0-2e67172889bb", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "LBTC depeg alert", - "description": "If the value is not 1e8, LBTC is undercollateralized.", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-06-18T14:28:03.112176Z", - "delivery_channels": [ - { - "delivery_channel_id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "delivery_channel": { - "id": "324eac53-e4e9-4bd6-9c80-abe612ff1ff9", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Renzo Alerts SAM", - "reference_id": "38bb1f0c-bf15-40a0-9117-9917679d37da", - "enabled": true, - "created_at": "2024-10-13T21:45:04.627275Z", - "information": { - "chat_id": -4591087023 - } - }, - "enabled": true, - "created_at": "2025-06-18T14:31:51.209342Z" - } - ], - "is_editable": true, - "updated_at": "2025-06-18T14:31:51.211791Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0xb415eaa355d8440ac7ecb602d3fb67ccc1f0bc81", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "view_function", - "expression": { - "address": "0xb415eaa355d8440ac7ecb602d3fb67ccc1f0bc81", - "input": "0x50d25bcd", - "parameter_condition": { - "parameter_name": "latestAnswer", - "operator": "", - "parameter_type": "int", - "nested_parameter_type": "", - "compare_change": true - }, - "network_id": "1" - } - } - ] - }, - { - "id": "fba03d32-4920-41c0-9e4c-8c007e8b000e", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Function queueTransaction called in InstaTimelock", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-08-22T12:50:48.783567Z", - "delivery_channels": [ - { - "delivery_channel_id": "b46df378-9ec3-4239-962c-44c0d40c0ce3", - "delivery_channel": { - "id": "b46df378-9ec3-4239-962c-44c0d40c0ce3", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Fluid Alerts SAM", - "reference_id": "6945b0b1-1b26-42c0-885b-fb015aa52b6d", - "enabled": true, - "created_at": "2025-08-22T12:50:11.414738Z", - "information": { - "chat_id": -1002941086824 - } - }, - "enabled": true, - "created_at": "2025-08-22T12:50:48.805868Z" - } - ], - "is_editable": true, - "updated_at": "2025-08-22T12:50:48.783568Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x2386dc45added673317ef068992f19421b481f4c", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "method_call", - "expression": { - "address": "0x2386dc45added673317ef068992f19421b481f4c", - "function_name": "queueTransaction", - "signature": { - "function_name": "queueTransaction", - "input_types": [ - "address", - "uint256", - "string", - "bytes", - "uint256" - ] - } - } - } - ] - } - ] -} \ No newline at end of file From 4860d0669d7f698c14deacdae2cb85f4c552cc7b Mon Sep 17 00:00:00 2001 From: spalen0 Date: Mon, 5 Oct 2026 22:52:16 +0200 Subject: [PATCH 2/2] docs: dedupe and fix stale docs - api/README.md now points to deploy/alerts-api.md instead of a stale copy - CONTRIBUTING: fix utils/logger.py and utils/http_client.py paths, Python 3.12+, SQLite-backed cache - CLAUDE.md: defer setup/style to CONTRIBUTING, use uv run, add test command - README: add 3Jane and Stablecoins, link monitoring site as source of truth - Dispatch doc: point to DISPATCHABLE_PROTOCOLS instead of a stale table - Timelock README: replace drifted address table and duplicate how-to with pointers, drop broken detils.md links, fix relative paths; SKILL.md absorbs the extra notes Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 8 +-- README.md | 4 ++ api/README.md | 104 ++---------------------------- deploy/emergency-dispatch-demo.md | 12 +--- protocols/timelock/README.md | 54 ++-------------- protocols/timelock/SKILL.md | 11 ++-- 6 files changed, 26 insertions(+), 167 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eff6d31..02e00af7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,13 +33,13 @@ Shared utilities live in `utils/`: | Module | Purpose | |---|---| -| `utils/logging.py` | Structured logging via `get_logger(name)` | +| `utils/logger.py` | Structured logging via `get_logger(name)` | | `utils/telegram.py` | Telegram alert delivery | -| `utils/cache.py` | File-based key:value persistence | +| `utils/cache.py` | Key:value state persistence between runs (SQLite-backed) | | `utils/web3_wrapper.py` | Web3 connection management (`ChainManager`) | | `utils/config.py` | Environment config (`Config`) | | `utils/formatting.py` | Number formatting helpers (`format_usd`, `format_token_amount`) | -| `utils/http.py` | HTTP request helper (`fetch_json`) | +| `utils/http_client.py` | HTTP helpers with retries (`fetch_json`, `request_with_retry`) | | `utils/chains.py` | Chain enum and explorer URLs | | `utils/abi.py` | ABI loader | | `utils/gauntlet.py` | Gauntlet risk parameter helpers | @@ -141,7 +141,7 @@ with client.batch_requests() as batch: ### Caching -Use `utils/cache.py` for persisting state between runs (e.g. last processed timestamp or proposal ID): +Use `utils/cache.py` for persisting state between runs (e.g. last processed timestamp or proposal ID). Values live in SQLite at `$CACHE_DIR/monitoring.db`; the filename argument is only used as a namespace: ```python from utils.cache import cache_filename, get_last_value_for_key_from_file, write_last_value_to_file diff --git a/README.md b/README.md index 5f560db5..6646fa88 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ Monitoring scripts for DeFi protocols to track key metrics and send alerts. Join ## Supported Protocols +Live status for every monitor is on [curation.yearn.fi/monitoring](https://curation.yearn.fi/monitoring/), generated from [`monitoring.yaml`](./monitoring.yaml). + +- [3Jane](./protocols/3jane/README.md) - [Aave V3](./protocols/aave/README.md) - [APYUSD](./protocols/apyusd/README.md) - [Bedrock uniBTC](./protocols/unibtc/README.md) @@ -32,6 +35,7 @@ Monitoring scripts for DeFi protocols to track key metrics and send alerts. Join - [Timelock Alerts](./protocols/timelock/README.md) — monitors OpenZeppelin `TimelockController` contracts for `CallScheduled` events across multiple protocols and sends Telegram alerts to protocol-specific channels. - [Safe Multisigs](./protocols/safe/main.py) — monitors Safe multisig wallets for queued transactions across multiple protocols. +- [Stablecoins](./protocols/stables/) — depeg alerts via DeFiLlama and Chainlink oracle health checks (staleness, round health, peg and market divergence). ## Telegram Alerts diff --git a/api/README.md b/api/README.md index 7f09b77d..89738b83 100644 --- a/api/README.md +++ b/api/README.md @@ -1,7 +1,7 @@ # Monitoring Alerts API -Read-only HTTP API for persisted monitoring alerts and the list of monitored -protocols. +Read-only HTTP API for persisted monitoring alerts, monitored protocols, and +monitoring card metadata. Run locally: @@ -9,101 +9,5 @@ Run locally: CACHE_DIR=/tmp/monitoring-cache uv run python -m api ``` -Production runs as `monitoring-api.service` and binds to `127.0.0.1:8923`. -Public auth and rate limiting should be handled by the reverse proxy. - -## Health - -```sh -curl http://127.0.0.1:8923/healthz -``` - -```json -{"status":"ok"} -``` - -## Protocols - -Returns enabled protocol objects from `automation/jobs.yaml`, grouped with the -tasks that monitor each protocol. - -```sh -curl http://127.0.0.1:8923/v1/protocols -``` - -```json -{ - "data": [ - { - "name": "aave", - "tasks": [ - { - "name": "aave", - "script": "protocols/aave/main.py", - "args": {}, - "profile": "hourly", - "cron": "5 * * * *" - } - ] - } - ], - "count": 1 -} -``` - -## Alerts - -```sh -curl 'http://127.0.0.1:8923/v1/alerts?limit=10' -curl 'http://127.0.0.1:8923/v1/alerts?source=protocol&protocol=aave' -curl 'http://127.0.0.1:8923/v1/alerts?from=2026-06-11T00:00:00Z&to=2026-06-12T00:00:00Z' -``` - -Query parameters: - -- `limit`: default `100`, max `500`. -- `cursor`: previous response `next_cursor`, for older rows. -- `from`: inclusive timestamp with timezone. -- `to`: exclusive timestamp with timezone. -- `since`: alias for `from`. -- `protocol`: exact protocol filter. -- `severity`: `LOW`, `MEDIUM`, `HIGH`, or `CRITICAL`. -- `source`: `protocol`, `ops_error`, `crash`, or `automation_digest`. - -Response: - -```json -{ - "data": [ - { - "id": 5021, - "created_at": "2026-06-11T10:23:45.123456Z", - "source": "protocol", - "protocol": "aave", - "channel": "aave", - "severity": "LOW", - "message": "message text", - "plain_text": false, - "silent": true, - "delivery_status": "delivered", - "delivered_at": "2026-06-11T10:23:45.456789Z", - "delivery_error": null, - "metadata": {} - } - ], - "next_cursor": "5021", - "limit": 100 -} -``` - -Fetch the next page: - -```sh -curl 'http://127.0.0.1:8923/v1/alerts?cursor=5021&limit=100' -``` - -Fetch one alert: - -```sh -curl http://127.0.0.1:8923/v1/alerts/5021 -``` +See [`deploy/alerts-api.md`](../deploy/alerts-api.md) for endpoints, response +shapes, pagination, and production setup. diff --git a/deploy/emergency-dispatch-demo.md b/deploy/emergency-dispatch-demo.md index 29805776..c787e29f 100644 --- a/deploy/emergency-dispatch-demo.md +++ b/deploy/emergency-dispatch-demo.md @@ -39,15 +39,7 @@ monitoring-scripts-py liquidity-monitoring ## Protocols with dispatch enabled -| Protocol | Telegram channel | Example alerts | -|---|---|---| -| infinifi | infinifi | Backing < 0.999, reserves < $15M | -| cap | cap | Withdrawable liquidity < $50M | -| ethena | ethena | USDe not fully backed | -| ethplus | rtoken | Coverage below threshold, StRSR rate drop | -| origin | pegs | Wrapped OETH redeem value drop, backing ratio drop | -| usdai | usdai | _(hook registered)_ | -| 3jane | 3jane | USD3/sUSD3 PPS decrease, junior buffer low, vault shutdown, protocol pause | +The authoritative list is `DISPATCHABLE_PROTOCOLS` in [`utils/dispatch.py`](../utils/dispatch.py). Only HIGH and CRITICAL alerts whose `protocol` is in that set trigger a dispatch. ## Safety mechanisms @@ -99,4 +91,4 @@ curl -sS -X POST http://127.0.0.1:8080/webhook/emergency \ --data-binary "$body" ``` -Replace `usdai` with any protocol from the list above. +Replace `usdai` with any protocol from `DISPATCHABLE_PROTOCOLS`. diff --git a/protocols/timelock/README.md b/protocols/timelock/README.md index aab5832a..cead5777 100644 --- a/protocols/timelock/README.md +++ b/protocols/timelock/README.md @@ -7,9 +7,9 @@ Monitors all timelock contract types (TimelockController, Aave, Compound, Lido, 1. Queries the Envio GraphQL indexer (`ENVIO_GRAPHQL_URL`) for new `TimelockEvent` events across all monitored timelocks (all types). 2. Groups events by `operationId` so batch operations (`scheduleBatch`) are sent as a single alert. 3. Routes each alert to the correct Telegram channel based on the protocol mapping. -4. Stores the latest processed `blockTimestamp` in `cache-id.txt` (key: `TIMELOCK_LAST_TS`) to avoid duplicate alerts between runs. +4. Stores the latest processed `blockTimestamp` in the SQLite state store (namespace `cache-id.txt`, key `TIMELOCK_LAST_TS`) to avoid duplicate alerts between runs. -The script runs hourly via the [monitoring runner](../automation/jobs.yaml). +The script runs hourly via the [monitoring runner](../../automation/jobs.yaml). ## Stale Ready Operations @@ -85,51 +85,13 @@ The `TimelockEvent` type includes fields that vary by timelock type: - **Lido**: `creator`, `metadata`, `operationId` (voteId) - **Maple**: `delay` (absolute timestamp/delayedUntil), `operationId` (proposalId) -For complete field mapping details, see [`detils.md`](./detils.md). - ## Monitored Timelocks -| Address | Chain | Protocol | Label | -|---------|-------|----------|-------| -| [0xd8236031d8279d82e615af2bfab5fc0127a329ab](https://etherscan.io/address/0xd8236031d8279d82e615af2bfab5fc0127a329ab) | Mainnet | CAP | CAP TimelockController | -| [0x5d8a7dc9405f08f14541ba918c1bf7eb2dace556](https://etherscan.io/address/0x5d8a7dc9405f08f14541ba918c1bf7eb2dace556) | Mainnet | RTOKEN | ETH+ Timelock | -| [0x055e84e7fe8955e2781010b866f10ef6e1e77e59](https://etherscan.io/address/0x055e84e7fe8955e2781010b866f10ef6e1e77e59) | Mainnet | LRT | Lombard TimeLock | -| [0x9f26d4c958fd811a1f59b01b86be7dffc9d20761](https://etherscan.io/address/0x9f26d4c958fd811a1f59b01b86be7dffc9d20761) | Mainnet | LRT | EtherFi Timelock | -| [0x49bd9989e31ad35b0a62c20be86335196a3135b1](https://etherscan.io/address/0x49bd9989e31ad35b0a62c20be86335196a3135b1) | Mainnet | LRT | KelpDAO(rsETH) Timelock | -| [0x3d18480cc32b6ab3b833dcabd80e76cfd41c48a9](https://etherscan.io/address/0x3d18480cc32b6ab3b833dcabd80e76cfd41c48a9) | Mainnet | INFINIFI | Infinifi Longtimelock | -| [0x4b174afbed7b98ba01f50e36109eee5e6d327c32](https://etherscan.io/address/0x4b174afbed7b98ba01f50e36109eee5e6d327c32) | Mainnet | INFINIFI | Infinifi Shorttimelock | -| [0x9aee0b04504cef83a65ac3f0e838d0593bcb2bc7](https://etherscan.io/address/0x9aee0b04504cef83a65ac3f0e838d0593bcb2bc7) | Mainnet | AAVE | Aave Governance V3 | -| [0x6d903f6003cca6255d85cca4d3b5e5146dc33925](https://etherscan.io/address/0x6d903f6003cca6255d85cca4d3b5e5146dc33925) | Mainnet | COMP | Compound Timelock | -| [0x2386dc45added673317ef068992f19421b481f4c](https://etherscan.io/address/0x2386dc45added673317ef068992f19421b481f4c) | Mainnet | FLUID | Fluid Timelock | -| [0x2e59a20f205bb85a89c53f1936454680651e618e](https://etherscan.io/address/0x2e59a20f205bb85a89c53f1936454680651e618e) | Mainnet | LIDO | Lido Timelock | -| [0x2efff88747eb5a3ff00d4d8d0f0800e306c0426b](https://etherscan.io/address/0x2efff88747eb5a3ff00d4d8d0f0800e306c0426b) | Mainnet | MAPLE | Maple GovernorTimelock | -| [0x1dccd4628d48a50c1a7adea3848bcc869f08f8c2](https://etherscan.io/address/0x1dccd4628d48a50c1a7adea3848bcc869f08f8c2) | Mainnet | 3JANE | 3Jane 24h TimelockController | -| [0x3d3c41419ab401cd25055e8f9421d7d96d887885](https://etherscan.io/address/0x3d3c41419ab401cd25055e8f9421d7d96d887885) | Mainnet | 3JANE | 3Jane 7d TimelockController | -| [0xf817cb3092179083c48c014688d98b72fb61464f](https://basescan.org/address/0xf817cb3092179083c48c014688d98b72fb61464f) | Base | LRT | superOETH Timelock | -| [0x88ba032be87d5ef1fbe87336b7090767f367bf73](https://etherscan.io/address/0x88ba032be87d5ef1fbe87336b7090767f367bf73) | Mainnet | YEARN | Yearn TimelockController | -| [0x88ba032be87d5ef1fbe87336b7090767f367bf73](https://basescan.org/address/0x88ba032be87d5ef1fbe87336b7090767f367bf73) | Base | YEARN | Yearn TimelockController | -| [0x88ba032be87d5ef1fbe87336b7090767f367bf73](https://arbiscan.io/address/0x88ba032be87d5ef1fbe87336b7090767f367bf73) | Arbitrum | YEARN | Yearn TimelockController | -| [0x88ba032be87d5ef1fbe87336b7090767f367bf73](https://polygonscan.com/address/0x88ba032be87d5ef1fbe87336b7090767f367bf73) | Polygon | YEARN | Yearn TimelockController | -| [0x88ba032be87d5ef1fbe87336b7090767f367bf73](https://katanascan.com/address/0x88ba032be87d5ef1fbe87336b7090767f367bf73) | Katana | YEARN | Yearn TimelockController | +The list of monitored timelocks (address, chain, protocol, label) is `TIMELOCK_LIST` in [`timelock_alerts.py`](./timelock_alerts.py). ## How to Add a New Timelock -1. **Add the address to the Envio indexer config.** The address must be indexed before this script can query events for it. Open the [Envio config.yaml](https://github.com/yearn/yearn-envio/blob/main/config.yaml), add the address under the correct chain's `TimelockController` contract list, and deploy the updated indexer. - -2. **Add a `TimelockConfig` entry** in [`timelock_alerts.py`](./timelock_alerts.py) in the `TIMELOCK_LIST` list: - -```python -(TimelockConfig("0xabcdef...lowercase_address", 1, "PROTOCOL_NAME", "Human Readable Label"),) -``` - -Parameters: -- **address**: Timelock contract address, **must be lowercase**. -- **chain_id**: Chain ID (`1` for Mainnet, `8453` for Base, etc.). Must match the network in the Envio config. -- **protocol**: Protocol identifier used for Telegram routing. Maps to `TELEGRAM_CHAT_ID_{PROTOCOL}` and `TELEGRAM_BOT_TOKEN_{PROTOCOL}` env variables. Falls back to `TELEGRAM_BOT_TOKEN_DEFAULT` if no protocol-specific bot token exists. -- **label**: Human-readable name shown in the alert message. - -3. If the chain is new, make sure it exists in [`utils/chains.py`](../utils/chains.py) (`Chain` enum and `EXPLORER_URLS` dict). -4. If the protocol needs a dedicated Telegram channel, add `TELEGRAM_CHAT_ID_{PROTOCOL}` and optionally `TELEGRAM_BOT_TOKEN_{PROTOCOL}` to the environment and GitHub Actions secrets. +Follow [`SKILL.md`](./SKILL.md). It covers the Envio indexer change, the `TimelockConfig` entry, and the deployment order. ## Alert Format @@ -185,7 +147,7 @@ For batch operations (`scheduleBatch`), all calls are included in a single messa ## Usage ```bash -uv run timelock/timelock_alerts.py +uv run protocols/timelock/timelock_alerts.py ``` Optional flags: @@ -198,8 +160,4 @@ Optional flags: ## Caching -The script stores the latest processed `blockTimestamp` in `cache-id.txt` under key `TIMELOCK_LAST_TS`. This value is universal across chains (unlike block numbers) so a single cache entry covers all monitored timelocks. On the first run (or with `--no-cache`), it falls back to querying events from the last 12 hours. - -## Schema Details - -For comprehensive information about the unified `TimelockEvent` schema, including field mappings for all supported timelock types (TimelockController, Aave, Compound, Lido, Maple), see [`detils.md`](./detils.md). +The script stores the latest processed `blockTimestamp` via `utils/cache.py` (namespace `cache-id.txt`, key `TIMELOCK_LAST_TS`). This value is universal across chains (unlike block numbers) so a single cache entry covers all monitored timelocks. On the first run (or with `--no-cache`), it falls back to querying events from the last 12 hours. diff --git a/protocols/timelock/SKILL.md b/protocols/timelock/SKILL.md index 8d132324..5f61e5a6 100644 --- a/protocols/timelock/SKILL.md +++ b/protocols/timelock/SKILL.md @@ -117,14 +117,16 @@ The indexer must be deployed and indexing events before the monitoring script ca **Repository**: [yearn/monitoring-scripts-py](https://github.com/yearn/monitoring-scripts-py) -### 3.1 — Add `TimelockConfig` entry in `timelock/timelock_alerts.py` +### 3.1 — Add `TimelockConfig` entry in `protocols/timelock/timelock_alerts.py` ```python (TimelockConfig("0xlowercase_address", chain_id, "PROTOCOL", "Human Label"),) ``` - Address **must be lowercase** -- Protocol name is used for Telegram routing (`TELEGRAM_CHAT_ID_{PROTOCOL}`) +- `chain_id` must match the network in the Envio config; a new chain must also exist in `utils/chains.py` (`Chain` enum and `EXPLORER_URLS`) +- Protocol name is used for Telegram routing (`TELEGRAM_CHAT_ID_{PROTOCOL}`, optional `TELEGRAM_BOT_TOKEN_{PROTOCOL}`, falling back to `TELEGRAM_BOT_TOKEN_DEFAULT`) +- If the routing key is not a website page key, map it in `ALERT_HISTORY_PROTOCOLS`; `tests/test_alert_protocol_keys.py` fails otherwise ### 3.2 — Handle delay format (only for new contract types) @@ -151,8 +153,7 @@ elif timelock_type in ("TimelockController", "Compound", "Puffer", "NewProtocol" ### 3.4 — Update documentation -- Add the new timelock to the table in `timelock/README.md` -- Update the `timelockType` list in the schema fields section +- For a new contract type, update the `timelockType` list in the schema fields section of `protocols/timelock/README.md` - If using a new protocol, add `TELEGRAM_CHAT_ID_{PROTOCOL}` to the monitoring env file (`/etc/monitoring/.env` on the VPS) and document it in `.env.example` ## Deployment Order @@ -166,7 +167,7 @@ elif timelock_type in ("TimelockController", "Compound", "Puffer", "NewProtocol" After both are deployed, run manually to verify: ```bash -uv run timelock/timelock_alerts.py --no-cache --since-seconds 604800 --log-level DEBUG +uv run protocols/timelock/timelock_alerts.py --no-cache --since-seconds 604800 --log-level DEBUG ``` This looks back 7 days to catch any recent events from the new timelock.