From 2c3901a25529b86a85af4140d00048d8def702c3 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Wed, 20 May 2026 16:06:06 +0200 Subject: [PATCH] chore: remove deprecated protocol monitors and related alerts Drops monitoring code, workflow secrets, timelock entries, and Tenderly alerts for protocols that are no longer being monitored. Removed protocols: - resolv (USR) - usd0 (Usual Money) - moonwell - silo Also removes pufETH (Puffer) and ezETH (Renzo) alerts from the LRT pegs monitor, timelock alerts, and Tenderly alert config. The Puffer-type branches in timelock_alerts.py are dropped since no Puffer-type timelock remains in the list. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/_run-monitoring.yml | 6 - README.md | 4 - lrt-pegs/README.md | 11 +- lrt-pegs/curve/main.py | 1 - lrt-pegs/fluid/main.py | 7 - moonwell/README.md | 29 - moonwell/bad_debt.py | 124 --- moonwell/proposals.py | 129 --- pyproject.toml | 6 +- resolv/README.md | 49 - resolv/abi/usr_price_storage.json | 743 ------------- resolv/abi/usr_redemption.json | 1254 ---------------------- resolv/resolv.py | 523 --------- safe/main.py | 24 - silo/README.md | 19 - silo/abi/SiloLens.json | 356 ------ silo/main.py | 129 --- silo/ur_sniff.py | 59 - timelock/README.md | 14 +- timelock/timelock_alerts.py | 10 +- usd0/README.md | 27 - usd0/main.py | 25 - usd0/price.py | 63 -- utils/tenderly/alerts_with_timelock.json | 178 --- 24 files changed, 12 insertions(+), 3778 deletions(-) delete mode 100644 moonwell/README.md delete mode 100644 moonwell/bad_debt.py delete mode 100644 moonwell/proposals.py delete mode 100644 resolv/README.md delete mode 100644 resolv/abi/usr_price_storage.json delete mode 100644 resolv/abi/usr_redemption.json delete mode 100644 resolv/resolv.py delete mode 100644 silo/README.md delete mode 100644 silo/abi/SiloLens.json delete mode 100644 silo/main.py delete mode 100644 silo/ur_sniff.py delete mode 100644 usd0/README.md delete mode 100644 usd0/main.py delete mode 100644 usd0/price.py diff --git a/.github/workflows/_run-monitoring.yml b/.github/workflows/_run-monitoring.yml index fe53f0d5..7d4959dd 100644 --- a/.github/workflows/_run-monitoring.yml +++ b/.github/workflows/_run-monitoring.yml @@ -57,7 +57,6 @@ env: TELEGRAM_BOT_TOKEN_LIDO: ${{ secrets.TELEGRAM_BOT_TOKEN_LIDO }} TELEGRAM_BOT_TOKEN_PEGS: ${{ secrets.TELEGRAM_BOT_TOKEN_PEGS }} TELEGRAM_BOT_TOKEN_PENDLE: ${{ secrets.TELEGRAM_BOT_TOKEN_PENDLE }} - TELEGRAM_BOT_TOKEN_SILO: ${{ secrets.TELEGRAM_BOT_TOKEN_SILO }} TELEGRAM_BOT_TOKEN_SPARK: ${{ secrets.TELEGRAM_BOT_TOKEN_SPARK }} TELEGRAM_BOT_TOKEN_STARGATE: ${{ secrets.TELEGRAM_BOT_TOKEN_STARGATE }} @@ -74,16 +73,12 @@ env: TELEGRAM_CHAT_ID_LRT: ${{ secrets.TELEGRAM_CHAT_ID_LRT }} TELEGRAM_CHAT_ID_MAPLE: ${{ secrets.TELEGRAM_CHAT_ID_MAPLE }} TELEGRAM_CHAT_ID_MAKER: ${{ secrets.TELEGRAM_CHAT_ID_MAKER }} - TELEGRAM_CHAT_ID_MOONWELL: ${{ secrets.TELEGRAM_CHAT_ID_MOONWELL }} TELEGRAM_CHAT_ID_MORPHO: ${{ secrets.TELEGRAM_CHAT_ID_MORPHO }} TELEGRAM_CHAT_ID_PENDLE: ${{ secrets.TELEGRAM_CHAT_ID_PENDLE }} TELEGRAM_CHAT_ID_PUFFER: ${{ secrets.TELEGRAM_CHAT_ID_PUFFER }} - TELEGRAM_CHAT_ID_RESOLV: ${{ secrets.TELEGRAM_CHAT_ID_RESOLV }} TELEGRAM_CHAT_ID_RTOKEN: ${{ secrets.TELEGRAM_CHAT_ID_RTOKEN }} - TELEGRAM_CHAT_ID_SILO: ${{ secrets.TELEGRAM_CHAT_ID_SILO }} TELEGRAM_CHAT_ID_SPARK: ${{ secrets.TELEGRAM_CHAT_ID_SPARK }} TELEGRAM_CHAT_ID_STARGATE: ${{ secrets.TELEGRAM_CHAT_ID_STARGATE }} - TELEGRAM_CHAT_ID_USD0: ${{ secrets.TELEGRAM_CHAT_ID_USD0 }} TELEGRAM_CHAT_ID_USDAI: ${{ secrets.TELEGRAM_CHAT_ID_USDAI }} TELEGRAM_CHAT_ID_YEARN: ${{ secrets.TELEGRAM_CHAT_ID_YEARN }} @@ -105,7 +100,6 @@ env: TELEGRAM_TOPIC_ID_MORPHO: ${{ vars.TELEGRAM_TOPIC_ID_MORPHO }} TELEGRAM_TOPIC_ID_PENDLE: ${{ vars.TELEGRAM_TOPIC_ID_PENDLE }} TELEGRAM_TOPIC_ID_RTOKEN: ${{ vars.TELEGRAM_TOPIC_ID_RTOKEN }} - TELEGRAM_TOPIC_ID_SILO: ${{ vars.TELEGRAM_TOPIC_ID_SILO }} TELEGRAM_TOPIC_ID_STRATA: ${{ vars.TELEGRAM_TOPIC_ID_STRATA }} TELEGRAM_TOPIC_ID_USTB: ${{ vars.TELEGRAM_TOPIC_ID_USTB }} TELEGRAM_TOPIC_ID_USDAI: ${{ vars.TELEGRAM_TOPIC_ID_USDAI }} diff --git a/README.md b/README.md index 31d08c9d..913a4f13 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,12 @@ Monitoring scripts for DeFi protocols to track key metrics and send alerts. Join - [LRTs](./lrt-pegs/README.md) - [Maple](./maple/README.md) - [Maker DAO](./maker/README.md) -- [Moonwell](./moonwell/README.md) - [Morpho](./morpho/README.md) - [Pendle](./pendle/README.md) -- [Resolv](./resolv/README.md) — _monitoring disabled_ - [RTokens - ETH+](./rtoken/README.md) -- [Silo](./silo/README.md) — _monitoring disabled_ - [Spark](./spark/README.md) - [Strata](./strata/README.md) - [Stargate](./stargate/README.md) — _monitoring disabled_ -- [USD0 - Usual Money](./usd0/README.md) - [USDAI](./usdai/README.md) - [USTB - Superstate](./ustb/README.md) - [Yearn](./yearn/README.md) diff --git a/lrt-pegs/README.md b/lrt-pegs/README.md index 006be809..5678f31b 100644 --- a/lrt-pegs/README.md +++ b/lrt-pegs/README.md @@ -2,13 +2,12 @@ ## Exchange rates -Checks the main liquidity pools of LRTs to detect depegging, such as the pools in Balancer and pufETH-wstETH, ETH+/WETH, ETH+/ETH pools in Curve. The bot monitors pool balances and sends a message if they become massively unbalanced. +Checks the main liquidity pools of LRTs to detect depegging, such as ETH+/WETH and ETH+/ETH pools in Curve. The bot monitors pool balances and sends a message if they become massively unbalanced. ### Curve pools Curve pools that are checked are: -- pufETH-wstETH - ETH+/WETH - weETH-WETH - frxETH-WETH @@ -48,14 +47,6 @@ This check runs hourly. [rsETH](https://etherscan.io/address/0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7#code) contract is upgradable proxy. Owner of the contrat [Timelock](https://etherscan.io/address/0x49bd9989e31ad35b0a62c20be86335196a3135b1), address: 0x49bd9989e31ad35b0a62c20be86335196a3135b1 with min delay set to [10 days](https://etherscan.io/address/0x49bd9989e31ad35b0a62c20be86335196a3135b1#readContract#F6). [Tenderly alert](https://dashboard.tenderly.co/yearn/sam/alerts/rules/c8108fff-b1f4-4cb0-abd3-c37ad541e6aa) and [internal timelock monitoring](../timelock/README.md) for CallScheduled events. -### Renzo (ezETH) - -[ezETH](https://etherscan.io/address/0xbf5495Efe5DB9ce00f80364C8B423567e58d2110#code) contract is upgradable proxy. The default admin role is set to the [Timelock](https://etherscan.io/address/0x4994EFc62101A9e3F885d872514c2dC7b3235849#readProxyContract#F17), address: 0x81f6e9914136da1a1d3b1efd14f7e0761c3d4cc7. [Tenderly alert](https://dashboard.tenderly.co/yearn/sam/alerts/rules/65153e56-1f79-45a2-8453-b61beeeab411) and [internal timelock monitoring](../timelock/README.md) for CallScheduled events. - -### Puffer Finance (pufETH) - -[pufETH](https://etherscan.io/address/0xD9A442856C234a39a81a089C06451EBAa4306a72#readProxyContract) contract is upgradable proxy. Contract [authority](https://etherscan.io/address/0xD9A442856C234a39a81a089C06451EBAa4306a72#readProxyContract#F7) is [AccessManager](https://etherscan.io/address/0x8c1686069474410E6243425f4a10177a94EBEE11#code) which admin is set to [Timelock contract](https://etherscan.io/address/0x3C28B7c7Ba1A1f55c9Ce66b263B33B204f2126eA). [Tenderly alert](https://dashboard.tenderly.co/yearn/sam/alerts/rules/f6654146-08d0-4a83-917a-23233be2314e) and [internal timelock monitoring](../timelock/README.md) for queueTransaction events. - ### Lombard Finance (LBTC) Monitoring [multisig of LBTC boring vault](https://etherscan.io/address/0xb7cB7131FFc18f87eEc66991BECD18f2FF70d2af) that can change all settings of Veda vault. [Tenderly alert](https://dashboard.tenderly.co/yearn/sam/alerts/rules/271040e6-85bc-4103-bf05-094a9912961a) and [internal timelock monitoring](../timelock/README.md) for CallScheduled events in [Lombard Timelock](https://etherscan.io/address/0x055e84e7fe8955e2781010b866f10ef6e1e77e59). This contract is the owner of [LBTC token](https://etherscan.io/token/0x8236a87084f8B84306f72007F36F2618A5634494#readProxyContract#F18). diff --git a/lrt-pegs/curve/main.py b/lrt-pegs/curve/main.py index 3d2767e6..a23df879 100644 --- a/lrt-pegs/curve/main.py +++ b/lrt-pegs/curve/main.py @@ -14,7 +14,6 @@ # Pool configurations POOL_CONFIGS = [ # name, pool address, index of lrt, index of other asset, peg threshold, protocol - ("pufETH-wstETH Curve Pool", "0xEEda34A377dD0ca676b9511EE1324974fA8d980D", 0, 1, THRESHOLD_RATIO, "puffer"), ("ETH+/WETH Curve Pool", "0x2c683fAd51da2cd17793219CC86439C1875c353e", 0, 1, THRESHOLD_RATIO, "ethplus"), ("OETH/ETH Curve Pool", "0xcc7d5785AD5755B6164e21495E07aDb0Ff11C2A8", 0, 1, THRESHOLD_RATIO, "origin"), # NOTE: bool is unbalanced, whole liquidity is moved to univ3: https://app.uniswap.org/explore/pools/ethereum/0x202a6012894ae5c288ea824cbc8a9bfb26a49b93 diff --git a/lrt-pegs/fluid/main.py b/lrt-pegs/fluid/main.py index ecfb06ed..e13bf9c2 100644 --- a/lrt-pegs/fluid/main.py +++ b/lrt-pegs/fluid/main.py @@ -28,13 +28,6 @@ 1, THRESHOLD_RATIO, ), - ( - "ezETH/ETH FLUID Pool", - "0xDD72157A021804141817d46D9852A97addfB9F59", - 0, - 1, - THRESHOLD_RATIO, - ), ( "weETH / ETH FLUID Pool", "0x86f874212335Af27C41cDb855C2255543d1499cE", diff --git a/moonwell/README.md b/moonwell/README.md deleted file mode 100644 index 60193e99..00000000 --- a/moonwell/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Moonwell - ⚠️ DISABLED MONITORING ⚠️ - -[Moonwell](https://moonwell.fi/) is Compound V2 fork. - -## Governance - -Tenderly alert for queueing tx to [Timelock contract on Base](https://dashboard.tenderly.co/yearn/sam/alerts/rules/41361042-facb-4d5d-b4a5-ddd1323e0602). - -[Comptroller contract](https://docs.moonwell.fi/moonwell/developers/comptroller) which handles upgrades and config updates is controlled by the [Timelock contract](https://basescan.org/address/0xfbb21d0380bee3312b33c4353c8936a0f13ef26c#readProxyContract#F2). Delay is [1 day](https://basescan.org/address/0x8b621804a7637b781e2BbD58e256a591F2dF7d51#readContract#F10). The Timelock contract can be paused by the [owner](https://basescan.org/address/0x8b621804a7637b781e2BbD58e256a591F2dF7d51#readContract#F7), which is multisig monitored by our bot. - -To get the proposal data from the received alert: - -1. see the tx from alert on [Tenderly](https://dashboard.tenderly.co/yearn/sam/tx/base/0x43f11101683eb6d58d346cc0f1c810f66abd1979539b0b54170904e5af64a310) -2. find the event `ProposalStateChanged` and get [`proposalId` value](https://basescan.org/tx/0x43f11101683eb6d58d346cc0f1c810f66abd1979539b0b54170904e5af64a310#eventlog) -3. go to Moonwell governance [https://moonwell.fi/governance/proposal/moonbeam?id=proposalId+79](https://moonwell.fi/governance/proposal/moonbeam?id=147) and check the proposal data. For base, the proposalId is increased by 79. - -The script [proposals.py](proposals.py) to check for new governance proposals is [run hourly by Github actions](../.github/workflows/hourly.yml#L100). If the proposal is queued, it sends a telegram message. It uses cache to avoid sending duplicate messages. - -## Data Monitoring - -The script [bad_debt.py](bad_debt.py) is run hourly by Github actions. It fetches the data from [Sentora API](https://defirisk.sentora.com/metrics/base/moonwell) and sends alerts if the thresholds are exceeded. If both data sources are not working, it sends an alert. - -### Bad Debt - -The alerts are sent when the [bad debt ratio](bad_debt.py#L65) is greater than 0.5% or if the [debt supply ratio](bad_debt.py#L66) is greater than 70%. The data is fetched from [Sentora API](https://defirisk.sentora.com/metrics/base/moonwell). - -### Debt Supply Ratio - -The script fetches the data from [Sentora API](https://defirisk.sentora.com/metrics/base/moonwell) and sends alerts if the debt supply ratio is greater than 70%. diff --git a/moonwell/bad_debt.py b/moonwell/bad_debt.py deleted file mode 100644 index 5b96f752..00000000 --- a/moonwell/bad_debt.py +++ /dev/null @@ -1,124 +0,0 @@ -from datetime import datetime, timedelta - -import requests - -from utils.logging import get_logger -from utils.telegram import send_telegram_message - -PROTOCOL = "moonwell" -logger = get_logger(PROTOCOL) - -BAD_DEBT_RATIO = 0.005 # 0.5% -DEBT_SUPPLY_RATIO = 0.70 # 70% - -BASE_URL = "https://services.defirisk.sentora.com/metric/base/moonwell" - - -def get_timestamp_before(hours: int): - """Get timestamp from hours ago in ISO format""" - now = datetime.utcnow() - hours_ago = now - timedelta(hours=hours) - return hours_ago.strftime("%Y-%m-%dT%H:00:00.000Z") - - -def fetch_metrics(): - """Fetch all required metrics from Sentora API about Moonwell""" - metrics = {} - error_messages = [] - endpoints = { - "total_supply": "general/total_supply", - "total_debt": "general/total_debt", - "bad_debt": "liquidation/health_factor_distribution", - } - - # Get timestamp from 48 hours ago because over the weekend the data is not updated. - timestamp = get_timestamp_before(hours=48) - - for metric_name, endpoint in endpoints.items(): - url = f"{BASE_URL}/{endpoint}?since={timestamp}" - try: - response = requests.get(url) - response.raise_for_status() - data = response.json() - - if not data.get("metric") or len(data["metric"]) == 0: - error_messages.append(f"No data returned for {metric_name}") - metrics[metric_name] = 0 - continue - - metrics[metric_name] = data["metric"][-1][1] # Get latest value - - except Exception as e: - error_messages.append(f"Error fetching {metric_name}: {str(e)}") - metrics[metric_name] = 0 - - # Send combined error messages if any - if error_messages: - combined_message = "Errors occurred:" + "\n".join(error_messages) - logger.error("%s", combined_message) - return {} - return metrics - - -def check_thresholds(metrics): - """Check if any metrics exceed thresholds and send alerts""" - total_supply = metrics["total_supply"] - total_debt = metrics["total_debt"] - bad_debt = metrics["bad_debt"] - - # If there is no supply or debt, skip the checks - if total_supply == 0 or total_debt == 0: - send_telegram_message("🚨 Moonwell metrics are all 0", PROTOCOL, disable_notification=True) - return - - tvl = total_supply - total_debt - - # Calculate ratios - bad_debt_ratio = bad_debt / tvl if tvl > 0 else 0 - debt_supply_ratio = total_debt / total_supply if total_supply > 0 else 0 - logger.info("Total supply: %s", f"{total_supply:,.2f}") - logger.info("Total debt: %s", f"{total_debt:,.2f}") - logger.info("TVL: %s", f"{tvl:,.2f}") - logger.info("Bad debt: %s", f"{bad_debt:,.2f}") - logger.info("Bad debt ratio: %s", f"{bad_debt_ratio:.2%}") - logger.info("Debt supply ratio: %s", f"{debt_supply_ratio:.2%}") - - alerts = [] - - # Check bad debt ratio - if bad_debt_ratio > BAD_DEBT_RATIO: - alerts.append( - f"🚨 High Bad Debt Alert:\n" - f"💀 Bad Debt Ratio: {bad_debt_ratio:.2%}\n" - f"💰 Bad Debt: ${bad_debt:,.2f}\n" - f"📊 TVL: ${tvl:,.2f}" - ) - - # Check debt/supply ratio - if debt_supply_ratio > DEBT_SUPPLY_RATIO: - alerts.append( - f"⚠️ High Debt/Supply Ratio Alert:\n" - f"📈 Debt/Supply Ratio: {debt_supply_ratio:.2%}\n" - f"💸 Total Debt: ${total_debt:,.2f}\n" - f"💰 Total Supply: ${total_supply:,.2f}" - ) - - if alerts: - message = "\n\n".join(alerts) - send_telegram_message(message, PROTOCOL) - - -def main(): - metrics = fetch_metrics() - if len(metrics) == 3: - check_thresholds(metrics) - else: - send_telegram_message( - "🚨 Moonwell metrics cannot be fetched from any source", - PROTOCOL, - disable_notification=True, - ) - - -if __name__ == "__main__": - main() diff --git a/moonwell/proposals.py b/moonwell/proposals.py deleted file mode 100644 index b23b4a4d..00000000 --- a/moonwell/proposals.py +++ /dev/null @@ -1,129 +0,0 @@ -import requests - -from utils.cache import get_last_queued_id_from_file, write_last_queued_id_to_file -from utils.logging import get_logger -from utils.telegram import send_telegram_message - -PROTOCOL = "moonwell" -logger = get_logger(PROTOCOL) - - -def fetch_moonwell_proposals(): - # Keep the original URL order but improve error handling - url = "https://ponder-eu2.moonwell.fi/graphql" - url_retry = "https://ponder.moonwell.fi/graphql" - - # Use the query that we know is working with at least the backup URL - query = """ - query { - proposals( - limit: 10, - orderDirection: "desc", - orderBy: "proposalId" - ) { - items { - id - proposalId - description - stateChanges(orderBy: "blockNumber") { - items { - newState - chainId - } - } - } - } - } - """ - payload = {"query": query} - use_retry_url = False - - try: - use_retry_url = False - try: - response = requests.post(url, json=payload) - response.raise_for_status() - - # Check for the specific schema error even when status code is 200 - data = response.json() - if "errors" in data and any( - 'relation "Proposal" does not exist' in error.get("message", "") for error in data.get("errors", []) - ): - logger.info("Primary URL returned schema error, switching to backup URL: %s", url_retry) - use_retry_url = True - raise requests.exceptions.RequestException("Schema error detected") - - except requests.exceptions.RequestException as e: - if not use_retry_url: - logger.info("Primary URL failed with error: %s, trying backup URL: %s", str(e), url_retry) - response = requests.post(url_retry, json=payload) - response.raise_for_status() - - data = response.json() - - # Check if the expected structure exists in the response - if "data" not in data: - logger.error("API returned no data. Response: %s", data) - return None - - if data["data"] is None or "proposals" not in data["data"]: - logger.error("API structure has changed. No 'proposals' in data. Response: %s", data) - return None - - if "items" not in data["data"]["proposals"]: - logger.error("API structure has changed. No 'items' in proposals. Response: %s", data) - return None - - base_proposals = [] - last_reported_id = get_last_queued_id_from_file(PROTOCOL) - - for proposal in data["data"]["proposals"]["items"]: - if "stateChanges" not in proposal or "items" not in proposal["stateChanges"]: - logger.info("Skipping proposal %s - missing stateChanges structure", proposal.get("proposalId")) - continue - - state_changes = proposal["stateChanges"]["items"] - for state_change in reversed(state_changes): - if state_change["chainId"] == 8453: - match state_change["newState"]: - case "EXECUTED": - break # skip executed proposals - case "QUEUED": - proposal_id = int(proposal["proposalId"]) - if proposal_id > last_reported_id: - base_proposals.append(proposal) - else: - logger.info("Proposal with id %s already sent", proposal_id) - break - - if not base_proposals: - logger.info("No new proposals found") - return None - - moonwell_proposal_url = "https://moonwell.fi/governance/proposal/moonbeam?id=" - message = "🌙 Moonwell Governance Proposals 🌙\n" - for proposal in base_proposals: - proposal_id = proposal["proposalId"] - proposal_url = moonwell_proposal_url + str(proposal_id) - message += f"🔗 Link to Proposal: {proposal_url}\n" - if proposal.get("description"): - # there is no title so we use the first line of the description - description = proposal["description"].split("\n")[0] - if len(description) > 500: # Still keep length limit for safety - description = description + "..." - message += f"📗 Title: {description}\n\n" - - send_telegram_message(message, PROTOCOL) - # write last sent queued proposal id to file - write_last_queued_id_to_file(PROTOCOL, base_proposals[0]["proposalId"]) - return base_proposals - - except requests.exceptions.RequestException as e: - # skip sending telegram message because tenderly alert is also set up for proposals - error_message = f"Failed to fetch moonwell proposals: {e}" - logger.error("%s", error_message) - return None - - -if __name__ == "__main__": - fetch_moonwell_proposals() diff --git a/pyproject.toml b/pyproject.toml index 7434bf2c..30b78d4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,9 +94,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools] packages = [ "aave", "bad-debt", "cap", "compound", "ethena", "euler", - "fluid", "infinifi", "lido", "lrt-pegs", "maker", "moonwell", - "maple", "morpho", "pendle", "resolv", "rtoken", - "safe", "silo", "spark", "stargate", "timelock", "usd0", "usdai", + "fluid", "infinifi", "lido", "lrt-pegs", "maker", + "maple", "morpho", "pendle", "rtoken", + "safe", "spark", "stargate", "timelock", "usdai", "utils", "yearn", ] diff --git a/resolv/README.md b/resolv/README.md deleted file mode 100644 index da69b12b..00000000 --- a/resolv/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# RESOLV Protocol Monitoring ⚠️ DISABLED ⚠️ - -The script [resolv/resolv.py](resolv.py) runs [hourly via GitHub Actions](../.github/workflows/hourly.yml) to monitor key health indicators of the RESOLV protocol using on-chain data. - -## Monitored Contracts - -- **USR Price Storage**: `0x7f45180d6fFd0435D8dD695fd01320E6999c261c` -- **USR Redemption**: `0x60A7B7915980ed34fDE6e239618fAdCf67897c37` - -The script [resolv/resolv.py](resolv.py) monitors several critical metrics: - -1. **USR Price Stability** - - Alerts if USR price deviates from 1e18 ($1.00) - - Monitors USR supply and reserves - - Alerts if over-collateralization is below 130% (i.e., if reserves are less than 130% of USR supply) - - Alerts if USR supply is zero or invalid - -2. **Redemption Usage** - - Tracks current redemption usage against redemption limit - - Alerts if usage exceeds 50% of the limit - - Uses smart caching to prevent spam alerts: - - Alerts on first run if above threshold - - Alerts when 24h reset is detected and usage is above threshold - - Alerts when threshold is crossed (goes from below to above 50%) - - Alerts once per 24h period if usage remains above threshold - -3. **Price Data Freshness** - - Monitors timestamp of last price update - - Alerts if data is older than 24 hours - -4. **Off-chain Reserves Dashboard** - - Pulls metrics from `https://info.apostro.xyz/resolv-reserves` - - Alerts if fetch fails or parsing fails - - Alerts if required fields are missing - - Alerts if reserves data timestamp is missing or unparseable - - Alerts if reserves data is older than 6 hours - - Alerts if USR over-collateralization is below 130% - - Alerts if market delta absolute value exceeds 6% - - Alerts if strategy net exposure exceeds 3% of TVL - - Alerts on negative percentage changes (drops only): - - TVL drop ≥ 10% - - USR TVL drop ≥ 10% - - RLP TVL drop ≥ 10% - - Backing assets value drop ≥ 5% - - Alerts if RLP/USR ratio changes by ≥ 5% (absolute) - -## Governance - -Monitor multisig: [0xD6889F307BE1b83Bb355d5DA7d4478FB0d2Af547](https://etherscan.io/address/0xD6889F307BE1b83Bb355d5DA7d4478FB0d2Af547) that manages all critical functions of the protocol. diff --git a/resolv/abi/usr_price_storage.json b/resolv/abi/usr_price_storage.json deleted file mode 100644 index 3979b80f..00000000 --- a/resolv/abi/usr_price_storage.json +++ /dev/null @@ -1,743 +0,0 @@ -[ - { - "inputs": [], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "AccessControlBadConfirmation", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint48", - "name": "schedule", - "type": "uint48" - } - ], - "name": "AccessControlEnforcedDefaultAdminDelay", - "type": "error" - }, - { - "inputs": [], - "name": "AccessControlEnforcedDefaultAdminRules", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "defaultAdmin", - "type": "address" - } - ], - "name": "AccessControlInvalidDefaultAdmin", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "neededRole", - "type": "bytes32" - } - ], - "name": "AccessControlUnauthorizedAccount", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidInitialization", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidKey", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidLowerBoundPercentage", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "price", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "lowerBound", - "type": "uint256" - } - ], - "name": "InvalidPrice", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidReserves", - "type": "error" - }, - { - "inputs": [], - "name": "InvalidUsrSupply", - "type": "error" - }, - { - "inputs": [], - "name": "NotInitializing", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "key", - "type": "bytes32" - } - ], - "name": "PriceAlreadySet", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "bits", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "SafeCastOverflowedUintDowncast", - "type": "error" - }, - { - "anonymous": false, - "inputs": [], - "name": "DefaultAdminDelayChangeCanceled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint48", - "name": "newDelay", - "type": "uint48" - }, - { - "indexed": false, - "internalType": "uint48", - "name": "effectSchedule", - "type": "uint48" - } - ], - "name": "DefaultAdminDelayChangeScheduled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [], - "name": "DefaultAdminTransferCanceled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "newAdmin", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint48", - "name": "acceptSchedule", - "type": "uint48" - } - ], - "name": "DefaultAdminTransferScheduled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint64", - "name": "version", - "type": "uint64" - } - ], - "name": "Initialized", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "lowerBoundPercentage", - "type": "uint256" - } - ], - "name": "LowerBoundPercentageSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "key", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "price", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "usrSupply", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "reserves", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - } - ], - "name": "PriceSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "previousAdminRole", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "newAdminRole", - "type": "bytes32" - } - ], - "name": "RoleAdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "RoleGranted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "RoleRevoked", - "type": "event" - }, - { - "inputs": [], - "name": "BOUND_PERCENTAGE_DENOMINATOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "DEFAULT_ADMIN_ROLE", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "PRICE_SCALING_FACTOR", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SERVICE_ROLE", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "acceptDefaultAdminTransfer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newAdmin", - "type": "address" - } - ], - "name": "beginDefaultAdminTransfer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "cancelDefaultAdminTransfer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint48", - "name": "newDelay", - "type": "uint48" - } - ], - "name": "changeDefaultAdminDelay", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "defaultAdmin", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "defaultAdminDelay", - "outputs": [ - { - "internalType": "uint48", - "name": "", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "defaultAdminDelayIncreaseWait", - "outputs": [ - { - "internalType": "uint48", - "name": "", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - } - ], - "name": "getRoleAdmin", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "grantRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "hasRole", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_lowerBoundPercentage", - "type": "uint256" - } - ], - "name": "initialize", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "lastPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "price", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "usrSupply", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reserves", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "lowerBoundPercentage", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingDefaultAdmin", - "outputs": [ - { - "internalType": "address", - "name": "newAdmin", - "type": "address" - }, - { - "internalType": "uint48", - "name": "schedule", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingDefaultAdminDelay", - "outputs": [ - { - "internalType": "uint48", - "name": "newDelay", - "type": "uint48" - }, - { - "internalType": "uint48", - "name": "schedule", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "key", - "type": "bytes32" - } - ], - "name": "prices", - "outputs": [ - { - "internalType": "uint256", - "name": "price", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "usrSupply", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reserves", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "renounceRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "revokeRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "rollbackDefaultAdminDelay", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_lowerBoundPercentage", - "type": "uint256" - } - ], - "name": "setLowerBoundPercentage", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_key", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "usrSupply", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reserves", - "type": "uint256" - } - ], - "name": "setReserves", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes4", - "name": "interfaceId", - "type": "bytes4" - } - ], - "name": "supportsInterface", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - } -] \ No newline at end of file diff --git a/resolv/abi/usr_redemption.json b/resolv/abi/usr_redemption.json deleted file mode 100644 index c86e3b81..00000000 --- a/resolv/abi/usr_redemption.json +++ /dev/null @@ -1,1254 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_usrTokenAddress", - "type": "address" - }, - { - "internalType": "address[]", - "name": "_allowedWithdrawalTokenAddresses", - "type": "address[]" - }, - { - "internalType": "contract ITreasury", - "name": "_treasury", - "type": "address" - }, - { - "internalType": "contract IChainlinkOracle", - "name": "_chainlinkOracle", - "type": "address" - }, - { - "internalType": "contract IUsrPriceStorage", - "name": "_usrPriceStorage", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_usrPriceStorageHeartbeatInterval", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_redemptionLimit", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_redemptionFeeBPS", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_lastResetTime", - "type": "uint256" - }, - { - "internalType": "string", - "name": "_version", - "type": "string" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "AccessControlBadConfirmation", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint48", - "name": "schedule", - "type": "uint48" - } - ], - "name": "AccessControlEnforcedDefaultAdminDelay", - "type": "error" - }, - { - "inputs": [], - "name": "AccessControlEnforcedDefaultAdminRules", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "defaultAdmin", - "type": "address" - } - ], - "name": "AccessControlInvalidDefaultAdmin", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "neededRole", - "type": "bytes32" - } - ], - "name": "AccessControlUnauthorizedAccount", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - } - ], - "name": "AddressEmptyCode", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "AddressInsufficientBalance", - "type": "error" - }, - { - "inputs": [], - "name": "EnforcedPause", - "type": "error" - }, - { - "inputs": [], - "name": "ExpectedPause", - "type": "error" - }, - { - "inputs": [], - "name": "FailedInnerCall", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "_idempotencyKey", - "type": "bytes32" - } - ], - "name": "IdempotencyKeyAlreadyExist", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - } - ], - "name": "InvalidAmount", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_lastResetTime", - "type": "uint256" - } - ], - "name": "InvalidLastResetTime", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_feeBPS", - "type": "uint256" - } - ], - "name": "InvalidRedemptionFee", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_token", - "type": "address" - } - ], - "name": "InvalidTokenAddress", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_price", - "type": "uint256" - } - ], - "name": "InvalidUsrPrice", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_limit", - "type": "uint256" - } - ], - "name": "RedemptionLimitExceeded", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "int256", - "name": "value", - "type": "int256" - } - ], - "name": "SafeCastOverflowedIntToUint", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint8", - "name": "bits", - "type": "uint8" - }, - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "SafeCastOverflowedUintDowncast", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "SafeCastOverflowedUintToInt", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "SafeERC20FailedOperation", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_token", - "type": "address" - } - ], - "name": "TokenAlreadyAllowed", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_token", - "type": "address" - } - ], - "name": "TokenNotAllowed", - "type": "error" - }, - { - "inputs": [], - "name": "UsrPriceHeartbeatIntervalCheckFailed", - "type": "error" - }, - { - "inputs": [], - "name": "ZeroAddress", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_tokenAddress", - "type": "address" - } - ], - "name": "AllowedWithdrawalTokenAdded", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_tokenAddres", - "type": "address" - } - ], - "name": "AllowedWithdrawalTokenRemoved", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_chainlinkOracleAddress", - "type": "address" - } - ], - "name": "ChainlinkOracleSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [], - "name": "DefaultAdminDelayChangeCanceled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint48", - "name": "newDelay", - "type": "uint48" - }, - { - "indexed": false, - "internalType": "uint48", - "name": "effectSchedule", - "type": "uint48" - } - ], - "name": "DefaultAdminDelayChangeScheduled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [], - "name": "DefaultAdminTransferCanceled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "newAdmin", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint48", - "name": "acceptSchedule", - "type": "uint48" - } - ], - "name": "DefaultAdminTransferScheduled", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "_sender", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "_receiver", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "_withdrawalToken", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "_withdrawalTokenAmount", - "type": "uint256" - } - ], - "name": "Redeemed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "_redemptionFeeBPS", - "type": "uint256" - } - ], - "name": "RedemptionFeeSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "_newResetTime", - "type": "uint256" - } - ], - "name": "RedemptionLimitReset", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "_redemptionLimit", - "type": "uint256" - } - ], - "name": "RedemptionLimitSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "previousAdminRole", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "bytes32", - "name": "newAdminRole", - "type": "bytes32" - } - ], - "name": "RoleAdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "RoleGranted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - } - ], - "name": "RoleRevoked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_treasuryAddress", - "type": "address" - } - ], - "name": "TreasurySet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "Unpaused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "_interval", - "type": "uint256" - } - ], - "name": "UsrPriceStorageHeartbeatIntervalSet", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "_usrPriceStorageAddress", - "type": "address" - } - ], - "name": "UsrPriceStorageSet", - "type": "event" - }, - { - "inputs": [], - "name": "DEFAULT_ADMIN_ROLE", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SERVICE_ROLE", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "USR_TOKEN_ADDRESS", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "acceptDefaultAdminTransfer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_allowedWithdrawalTokenAddress", - "type": "address" - } - ], - "name": "addAllowedWithdrawalToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "allowedWithdrawalTokens", - "outputs": [ - { - "internalType": "bool", - "name": "isAllowed", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "newAdmin", - "type": "address" - } - ], - "name": "beginDefaultAdminTransfer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "cancelDefaultAdminTransfer", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "chainlinkOracle", - "outputs": [ - { - "internalType": "contract IChainlinkOracle", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint48", - "name": "newDelay", - "type": "uint48" - } - ], - "name": "changeDefaultAdminDelay", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "currentRedemptionUsage", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "defaultAdmin", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "defaultAdminDelay", - "outputs": [ - { - "internalType": "uint48", - "name": "", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "defaultAdminDelayIncreaseWait", - "outputs": [ - { - "internalType": "uint48", - "name": "", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_withdrawalTokenAddress", - "type": "address" - } - ], - "name": "getRedeemPrice", - "outputs": [ - { - "internalType": "uint80", - "name": "roundId", - "type": "uint80" - }, - { - "internalType": "int256", - "name": "price", - "type": "int256" - }, - { - "internalType": "uint256", - "name": "startedAt", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "updatedAt", - "type": "uint256" - }, - { - "internalType": "uint80", - "name": "answeredInRound", - "type": "uint80" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - } - ], - "name": "getRoleAdmin", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "grantRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "hasRole", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "lastResetTime", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "paused", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingDefaultAdmin", - "outputs": [ - { - "internalType": "address", - "name": "newAdmin", - "type": "address" - }, - { - "internalType": "uint48", - "name": "schedule", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "pendingDefaultAdminDelay", - "outputs": [ - { - "internalType": "uint48", - "name": "newDelay", - "type": "uint48" - }, - { - "internalType": "uint48", - "name": "schedule", - "type": "uint48" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_withdrawalTokenAddress", - "type": "address" - } - ], - "name": "redeem", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_amount", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_receiver", - "type": "address" - }, - { - "internalType": "address", - "name": "_withdrawalTokenAddress", - "type": "address" - } - ], - "name": "redeem", - "outputs": [ - { - "internalType": "uint256", - "name": "withdrawalTokenAmount", - "type": "uint256" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "redeemCounter", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "redemptionFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "redemptionLimit", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "_allowedWithdrawalTokenAddress", - "type": "address" - } - ], - "name": "removeAllowedWithdrawalToken", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "renounceRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "role", - "type": "bytes32" - }, - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "revokeRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "rollbackDefaultAdminDelay", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IChainlinkOracle", - "name": "_chainlinkOracle", - "type": "address" - } - ], - "name": "setChainlinkOracle", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_redemptionFeeBPS", - "type": "uint256" - } - ], - "name": "setRedemptionFee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_redemptionLimit", - "type": "uint256" - } - ], - "name": "setRedemptionLimit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract ITreasury", - "name": "_treasury", - "type": "address" - } - ], - "name": "setTreasury", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IUsrPriceStorage", - "name": "_usrPriceStorage", - "type": "address" - } - ], - "name": "setUsrPriceStorage", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_usrPriceStorageHeartbeatInterval", - "type": "uint256" - } - ], - "name": "setUsrPriceStorageHeartbeatInterval", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes4", - "name": "interfaceId", - "type": "bytes4" - } - ], - "name": "supportsInterface", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "treasury", - "outputs": [ - { - "internalType": "contract ITreasury", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "unpause", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "usrPriceStorage", - "outputs": [ - { - "internalType": "contract IUsrPriceStorage", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "usrPriceStorageHeartbeatInterval", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] \ No newline at end of file diff --git a/resolv/resolv.py b/resolv/resolv.py deleted file mode 100644 index d11bdad6..00000000 --- a/resolv/resolv.py +++ /dev/null @@ -1,523 +0,0 @@ -import os -import re -import time -from datetime import datetime, timedelta - -import requests - -from utils.abi import load_abi -from utils.cache import ( - cache_filename, - get_last_value_for_key_from_file, - write_last_value_to_file, -) -from utils.chains import Chain -from utils.logging import get_logger -from utils.telegram import send_telegram_message -from utils.web3_wrapper import ChainManager - -PROTOCOL = "resolv" -logger = get_logger(PROTOCOL) - -USR_PRICE_STORAGE = "0x7f45180d6fFd0435D8dD695fd01320E6999c261c" -USR_REDEMPTION = "0x60A7B7915980ed34fDE6e239618fAdCf67897c37" - -ABI_USR_PRICE_STORAGE = load_abi("resolv/abi/usr_price_storage.json") -ABI_USR_REDEMPTION = load_abi("resolv/abi/usr_redemption.json") - -RESOLV_RESERVES_URL = "https://info.apostro.xyz/resolv-reserves" -REQUEST_TIMEOUT = 15 - -USR_OVER_COLLATERALIZATION_MIN_PCT = 130.0 -MARKET_DELTA_ABS_PCT_TRIGGER = 6.0 -NET_EXPOSURE_TVL_RATIO_TRIGGER = 0.03 -RESERVES_DATA_MAX_AGE_HOURS = 6 -ONE_DAY_SECONDS = 24 * 60 * 60 -WEI_PER_ETHER = 1e18 - -TVL_CHANGE_RATIO_TRIGGER = 0.1 -USR_TVL_CHANGE_RATIO_TRIGGER = 0.1 -RLP_TVL_CHANGE_RATIO_TRIGGER = 0.1 -BACKING_ASSETS_CHANGE_RATIO_TRIGGER = 0.05 -RLP_USR_RATIO_PCT_CHANGE_TRIGGER = 5.0 - - -def get_redemption_cache() -> tuple[int | None, int | None]: - """Returns (usage, last_reset_time) or (None, None) if no cache""" - cache_data = get_last_value_for_key_from_file(cache_filename, PROTOCOL) - if cache_data == 0: - return None, None - - parts = str(cache_data).split("|") - if len(parts) == 2: - return int(parts[0]), int(parts[1]) - - -def write_redemption_cache(usage: int, reset_time: int) -> None: - """Write redemption cache data using existing cache system""" - cache_value = f"{usage}|{reset_time}" - write_last_value_to_file(cache_filename, PROTOCOL, cache_value) - - -def should_alert_redemption(current_usage: int, redemption_limit: int) -> bool: - """ - Smart caching logic that only writes to cache when necessary: - - On first run - - When reset is detected - - When threshold is crossed (alert triggered) - - When 24+ hours have passed since last reset and data is still over threshold - """ - cached_usage, last_reset_time = get_redemption_cache() - current_time = int(time.time()) - threshold = redemption_limit / 2 - - # First run - save cache and check threshold - if cached_usage is None: - write_redemption_cache(current_usage, current_time) - return current_usage > threshold - - # Detect reset: current usage < cached usage indicates 24h reset happened - reset_detected = current_usage < cached_usage - - if reset_detected: - # Reset detected - save new state and check threshold - write_redemption_cache(current_usage, current_time) - return current_usage > threshold - - # Check if threshold crossed since last cache - threshold_crossed = (cached_usage <= threshold) and (current_usage > threshold) - - if threshold_crossed: - # Threshold crossed - save cache and alert - write_redemption_cache(current_usage, last_reset_time) - return True - - # Time-based alert: if above threshold and 24+ hours since last reset - time_since_reset = current_time - last_reset_time - if current_usage > threshold and time_since_reset >= ONE_DAY_SECONDS: - # Update reset time to prevent spam (alert once per 24h period) - logger.info("Data over threshold for 24+ hours") - write_redemption_cache(current_usage, current_time) - return True - - # No significant change - don't save cache, just continue monitoring - logger.info( - "Cached usage: %s, Last reset time: %s, Current usage: %s, Threshold: %s", - cached_usage, - last_reset_time, - current_usage, - threshold, - ) - return False - - -def _reserves_cache_key(metric: str) -> str: - return f"{PROTOCOL}_RESERVES_{metric}" - - -def _get_cached_float(cache_key: str) -> float | None: - cached = get_last_value_for_key_from_file(cache_filename, cache_key) - if cached == 0: - return None - try: - return float(cached) - except ValueError: - return None - - -def _set_cached_float(cache_key: str, value: float) -> None: - write_last_value_to_file(cache_filename, cache_key, f"{value}") - - -def _strip_html(text: str) -> str: - return re.sub(r"<[^>]+>", " ", text) - - -def _normalize_text(text: str) -> str: - text = _strip_html(text) - text = text.replace("−", "-").replace("–", "-").replace("—", "-") - text = re.sub(r"\[\d+\]", "", text) - return re.sub(r"\s+", " ", text).strip() - - -def _extract_value(pattern: str, text: str) -> str | None: - match = re.search(pattern, text, re.IGNORECASE) - if not match: - return None - return match.group(1).strip() - - -def _parse_compact_usd(raw: str) -> float: - value = raw.replace("$", "").replace(",", "").replace(" ", "").strip() - sign = -1 if value.startswith("-") else 1 - value = value.lstrip("-").lstrip("+") - if value.startswith("−"): - sign = -1 - value = value.lstrip("−") - multiplier = 1.0 - if value.endswith("K"): - multiplier = 1_000.0 - value = value[:-1] - elif value.endswith("M"): - multiplier = 1_000_000.0 - value = value[:-1] - elif value.endswith("B"): - multiplier = 1_000_000_000.0 - value = value[:-1] - return sign * float(value) * multiplier - - -def _parse_percent(raw: str) -> float: - return float(raw.replace("%", "").replace(",", "").strip()) - - -def _format_usd(value: float) -> str: - return f"${value:,.2f}" - - -def _parse_reserves_timestamp(text: str) -> datetime | None: - match = re.search(r"RESOLV PROOF OF RESERVES\s+(\d{1,2}\s+[A-Za-z]{3}\s+\d{2}:\d{2}\s+UTC)", text) - if not match: - return None - raw = match.group(1) - year = datetime.utcnow().year - try: - ts = datetime.strptime(f"{raw} {year}", "%d %b %H:%M UTC %Y") - except ValueError: - return None - if ts > datetime.utcnow() + timedelta(days=1): - ts = ts.replace(year=year - 1) - return ts - - -def fetch_resolv_reserves_html() -> str | None: - """Fetch Resolv reserves page HTML.""" - try: - resp = requests.get(RESOLV_RESERVES_URL, timeout=REQUEST_TIMEOUT) - if resp.status_code != 200: - logger.error("HTTP %s for %s", resp.status_code, RESOLV_RESERVES_URL) - return None - return resp.text - except Exception as e: - logger.error("Failed to fetch %s: %s", RESOLV_RESERVES_URL, e) - return None - - -def parse_resolv_reserves_metrics(html: str) -> tuple[dict[str, float], list[str], datetime | None]: - text = _normalize_text(html) - metrics: dict[str, float] = {} - missing: list[str] = [] - - def _add_metric(label: str, pattern: str, parser, key: str) -> None: - raw = _extract_value(pattern, text) - if raw is None: - missing.append(label) - return - try: - metrics[key] = parser(raw) - except ValueError: - missing.append(label) - - _add_metric("TVL", r"(? None: - """Check if metric dropped by ratio threshold and alert if so.""" - cache_key = _reserves_cache_key(metric_key) - cached = cache.get(cache_key) - if cached is not None and cached > 0: - change_ratio = (current - cached) / cached - if change_ratio <= -ratio_trigger: - error_messages.append( - f"⚠️ Resolv reserves {label} changed by {change_ratio * 100:.2f}% " - f"({_format_usd(cached)} → {_format_usd(current)})" - ) - cache[cache_key] = current - - -def _check_metric_change_abs( - metric_key: str, - label: str, - current: float, - abs_trigger: float, - error_messages: list[str], - cache: dict[str, float | None], - unit: str = "%", -) -> None: - cache_key = _reserves_cache_key(metric_key) - cached = cache.get(cache_key) - if cached is not None: - delta = abs(current - cached) - if delta >= abs_trigger: - error_messages.append( - f"⚠️ Resolv reserves {label} moved by {delta:.2f}{unit} ({cached:.2f}{unit} → {current:.2f}{unit})" - ) - cache[cache_key] = current - - -def _load_all_cache_values(metric_keys: list[str]) -> dict[str, float | None]: - """Load all cache values at once to avoid repeated file I/O.""" - cache: dict[str, float | None] = {} - cache_keys = [_reserves_cache_key(key) for key in metric_keys] - - # Read cache file once - if not os.path.exists(cache_filename): - return {key: None for key in cache_keys} - - # Parse entire file once - file_cache: dict[str, str] = {} - with open(cache_filename, "r") as f: - for line in f: - line = line.strip() - if ":" in line: - key, value = line.split(":", 1) - file_cache[key] = value - - # Extract only the keys we need - for cache_key in cache_keys: - cached_value = file_cache.get(cache_key, "0") - if cached_value == "0": - cache[cache_key] = None - else: - try: - cache[cache_key] = float(cached_value) - except ValueError: - cache[cache_key] = None - - return cache - - -def _save_all_cache_values(cache: dict[str, float | None]) -> None: - """Save all cache values at once to avoid repeated file I/O.""" - if not cache: - return - - # Read existing cache file - file_cache: dict[str, str] = {} - if os.path.exists(cache_filename): - with open(cache_filename, "r") as f: - for line in f: - line = line.strip() - if ":" in line: - key, value = line.split(":", 1) - file_cache[key] = value - - # Update with new values - for cache_key, value in cache.items(): - if value is not None: - file_cache[cache_key] = f"{value}" - - # Write entire file once - with open(cache_filename, "w") as f: - for key, value in file_cache.items(): - f.write(f"{key}:{value}\n") - - -def process_resolv_reserves_metrics(error_messages: list[str]) -> None: - html = fetch_resolv_reserves_html() - if html is None: - error_messages.append("⚠️ Resolv reserves: failed to fetch off-chain data.") - return - - metrics, missing, timestamp = parse_resolv_reserves_metrics(html) - if missing: - error_messages.append(f"⚠️ Resolv reserves: missing fields: {', '.join(missing)}") - if not metrics: - error_messages.append("⚠️ Resolv reserves: failed to parse any metrics.") - return - - if timestamp is None: - error_messages.append("⚠️ Resolv reserves: timestamp missing or unparseable.") - else: - max_age = timedelta(hours=RESERVES_DATA_MAX_AGE_HOURS) - - if datetime.utcnow() - timestamp > max_age: - error_messages.append(f"⚠️ Resolv reserves data is stale: {timestamp} UTC") - - if "usr_over_collateralization_pct" in metrics: - if metrics["usr_over_collateralization_pct"] < USR_OVER_COLLATERALIZATION_MIN_PCT: - error_messages.append( - f"⚠️ Resolv USR over-collateralization below threshold: {metrics['usr_over_collateralization_pct']:.2f}%" - ) - - if "market_delta_pct" in metrics and abs(metrics["market_delta_pct"]) > MARKET_DELTA_ABS_PCT_TRIGGER: - error_messages.append( - "⚠️ Resolv market delta out of range: " - f"{metrics['market_delta_pct']:.2f}% (threshold {MARKET_DELTA_ABS_PCT_TRIGGER:.2f}%)" - ) - - if "strategy_net_exposure_usd" in metrics and "tvl_usd" in metrics and metrics["tvl_usd"] > 0: - net_ratio = abs(metrics["strategy_net_exposure_usd"]) / metrics["tvl_usd"] - if net_ratio > NET_EXPOSURE_TVL_RATIO_TRIGGER: - error_messages.append( - "⚠️ Resolv strategy net exposure too large: " - f"{_format_usd(metrics['strategy_net_exposure_usd'])} " - f"({net_ratio * 100:.2f}% of TVL)" - ) - - # Load all cache values once - metric_keys_to_check = ["tvl_usd", "usr_tvl_usd", "rlp_tvl_usd", "backing_assets_usd", "rlp_usr_ratio_pct"] - cache = _load_all_cache_values(metric_keys_to_check) - - # Check percentage-based change metrics - ratio_metrics = [ - ("tvl_usd", "TVL", TVL_CHANGE_RATIO_TRIGGER), - ("usr_tvl_usd", "USR TVL", USR_TVL_CHANGE_RATIO_TRIGGER), - ("rlp_tvl_usd", "RLP TVL", RLP_TVL_CHANGE_RATIO_TRIGGER), - ("backing_assets_usd", "backing assets value", BACKING_ASSETS_CHANGE_RATIO_TRIGGER), - ] - for metric_key, label, trigger in ratio_metrics: - if metric_key in metrics: - _check_metric_change_ratio(metric_key, label, metrics[metric_key], trigger, error_messages, cache) - - # Check absolute change metric (percentage points) - if "rlp_usr_ratio_pct" in metrics: - _check_metric_change_abs( - "rlp_usr_ratio_pct", - "RLP/USR ratio", - metrics["rlp_usr_ratio_pct"], - RLP_USR_RATIO_PCT_CHANGE_TRIGGER, - error_messages, - cache, - ) - - # Save all cache values once - _save_all_cache_values(cache) - - -def main() -> None: - client = ChainManager.get_client(Chain.MAINNET) - - try: - usr_price_storage = client.eth.contract(address=USR_PRICE_STORAGE, abi=ABI_USR_PRICE_STORAGE) - usr_redemption = client.eth.contract(address=USR_REDEMPTION, abi=ABI_USR_REDEMPTION) - except Exception as e: - error_message = f"Error creating contract instances: {e}. Check ABI paths and contract addresses." - logger.error("%s", error_message) - return # Cannot proceed without contracts - - # Combined blockchain calls - try: - with client.batch_requests() as batch: - batch.add(usr_redemption.functions.redemptionLimit()) - batch.add(usr_redemption.functions.currentRedemptionUsage()) - batch.add(usr_price_storage.functions.lastPrice()) - - responses = client.execute_batch(batch) - - if len(responses) != 3: - error_message = f"Batch Call: Expected 3 responses, got {len(responses)}" - logger.error("%s", error_message) - send_telegram_message(error_message, PROTOCOL, True, True) - return - - redemption_limit, current_redemption_usage, usr_last_price = responses - logger.info( - "Raw Data - Redemption Limit: %s, Current Redemption Usage: %s, USR Last Price: %s", - redemption_limit, - current_redemption_usage, - usr_last_price, - ) - usr_price, usr_supply, reserves, timestamp = usr_last_price - - except Exception as e: - error_message = f"Error during batch blockchain calls: {e}" - send_telegram_message(error_message, PROTOCOL, True, True) - return - - error_messages = [] - - if usr_price != WEI_PER_ETHER: - error_messages.append( - f"USR Price is not {WEI_PER_ETHER}!\n" - f"USR Price: {usr_price / WEI_PER_ETHER:.4f}\n" - f"USR Supply: {usr_supply / WEI_PER_ETHER:.4f}\n" - f"Reserves: {reserves / WEI_PER_ETHER:.4f}\n" - f"Timestamp: {timestamp}" - ) - - if usr_supply > 0: - over_collateralization_pct = (reserves / usr_supply) * 100 - - # Cache key for over-collateralization - cache_key_collateral = f"{PROTOCOL}_usr_over_collateralization" - last_collateral = _get_cached_float(cache_key_collateral) - - if over_collateralization_pct < USR_OVER_COLLATERALIZATION_MIN_PCT: - # Alert if no previous cache (first drop) or if value has fallen further - if last_collateral is None or over_collateralization_pct < last_collateral: - error_messages.append( - "USR over-collateralization below threshold!\n" - f"Over-collateralization: {over_collateralization_pct:.2f}%\n" - f"USR Supply: {usr_supply / WEI_PER_ETHER:.4f}\n" - f"Reserves: {reserves / WEI_PER_ETHER:.4f}" - ) - # Update cache with new low - _set_cached_float(cache_key_collateral, over_collateralization_pct) - else: - # Reset cache if healthy - if last_collateral is not None: - _set_cached_float(cache_key_collateral, 0) - else: - error_messages.append( - "USR supply is zero or invalid, cannot compute over-collateralization.\n" - f"USR Supply: {usr_supply}\n" - f"Reserves: {reserves}" - ) - - if should_alert_redemption(current_redemption_usage, redemption_limit): - error_messages.append( - "Current Redemption Usage is greater than 50% of Redemption Limit!\n" - f"Current Redemption Usage: {current_redemption_usage / WEI_PER_ETHER:.4f}\n" - f"Redemption Limit: {redemption_limit / WEI_PER_ETHER:.4f}\n" - f"Available redemption: {(redemption_limit - current_redemption_usage) / WEI_PER_ETHER:.4f}" - ) - - # Check if timestamp is older than one day - current_time = int(time.time()) - if timestamp < current_time - ONE_DAY_SECONDS: - error_messages.append( - f"⚠️ USR data is stale!\n" - f"Last update: {datetime.fromtimestamp(timestamp)}\n" - f"Current time: {datetime.fromtimestamp(current_time)}" - ) - - process_resolv_reserves_metrics(error_messages) - - if error_messages: - send_telegram_message("\n".join(error_messages), PROTOCOL) - - -if __name__ == "__main__": - main() diff --git a/safe/main.py b/safe/main.py index 9a54cc90..4659f870 100644 --- a/safe/main.py +++ b/safe/main.py @@ -91,12 +91,6 @@ "0x84258B3C495d8e9b10D0d4A7867392F149Da4274", "Morpho eUSDe predeposit vault owner", ], # eUSDe predeposit vault owner, token used by DAI vault on morpho - # [ - # "RESOLV", - # "mainnet", - # "0xD6889F307BE1b83Bb355d5DA7d4478FB0d2Af547", - # "RESOLV contract", - # ], [ "LRT", "mainnet", @@ -145,30 +139,12 @@ "0xA27cA9292268ee0f0258B749f1D5740c9Bb68B50", "Strata Admin Multisig (3/4)", ], - # NOTE: Moonwell multisig monitoring is disabled for now - # [ - # "MOONWELL", - # "base-main", - # "0x446342AF4F3bCD374276891C6bb3411bf2F8779E", - # "Moonwell Admin of timelock controller", - # ], # admin of timelock controller - # [ - # "MOONWELL", - # "base-main", - # "0xB9d4acf113a423Bc4A64110B8738a52E51C2AB38", - # "Moonwell Pause guardian of comptroller contract", - # ], # pause guardian of comptroller contract # [ # "INFINIFI", # "mainnet", # "0x80608f852D152024c0a2087b16939235fEc2400c", # "Infinifi Team Multisig", # ], - # [ - # "USD0", - # "mainnet", - # "0x6e9d65eC80D69b1f508560Bc7aeA5003db1f7FB7", - # ], # USD0 protocol governance # no active stargate strategies # ["STARGATE", "mainnet", "0x65bb797c2B9830d891D87288F029ed8dACc19705"], # ["STARGATE", "polygon-main", "0x47290DE56E71DC6f46C26e50776fe86cc8b21656"], diff --git a/silo/README.md b/silo/README.md deleted file mode 100644 index 961698af..00000000 --- a/silo/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Silo - ⚠️ DISABLED ⚠️ - -## Bad Debt - -Github actions run hourly and send telegram message if there are positions with `riskFactor > 1` which can lead to bad debt. -[Python script code](https://github.com/tapired/monitoring-scripts-py/blob/main/silo/main.py) - -## Governance - -- [Internal timelock monitoring](../timelock/README.md) to send Telegram message for scheduled transactions in Timelock controller. Min delay is set to [2 days](https://etherscan.io/address/0xe1F03b7B0eBf84e9B9f62a1dB40f1Efb8FaA7d22#readContract#F5). -- Github actions run hourly and send telegram message when there are queued transactions in Safe Multisig (3/6). - -Important contracts such as SiloRepository and SiloRouter are owned by the Safe Multisigs listed below, and we monitor the pending transactions of these multisigs every hour. In the future, SILO will transition to on-chain voting with the Timelock contract and veSILO. Details can be found [here](https://gov.silo.finance/t/silo-finance-2024-roadmap/451). - -Currently, there is a [Timelock contract](0xe1F03b7B0eBf84e9B9f62a1dB40f1Efb8FaA7d22) deployed. However, it does not own any of the important contracts as the transition to complete on-chain governance is not yet complete. - - Mainnet Safe Multisig: 0xE8e8041cB5E3158A0829A19E014CA1cf91098554 - Optimism Safe Multisig: 0x468CD12aa9e9fe4301DB146B0f7037831B52382d - Arbitrum Safe Multisig: 0x865A1DA42d512d8854c7b0599c962F67F5A5A9d9 diff --git a/silo/abi/SiloLens.json b/silo/abi/SiloLens.json deleted file mode 100644 index ce3e3dd5..00000000 --- a/silo/abi/SiloLens.json +++ /dev/null @@ -1,356 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "contract ISiloRepository", - "name": "_siloRepo", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { "inputs": [], "name": "DifferentArrayLength", "type": "error" }, - { "inputs": [], "name": "InvalidRepository", "type": "error" }, - { "inputs": [], "name": "UnsupportedLTVType", "type": "error" }, - { "inputs": [], "name": "UserIsZero", "type": "error" }, - { "inputs": [], "name": "ZeroAssets", "type": "error" }, - { "inputs": [], "name": "ZeroAssets", "type": "error" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_assetTotalDeposits", - "type": "uint256" - }, - { - "internalType": "contract IShareToken", - "name": "_shareToken", - "type": "address" - }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "balanceOfUnderlying", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "borrowAPY", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "borrowShare", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "uint256", "name": "_amount", "type": "uint256" } - ], - "name": "calcFee", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "calculateBorrowValue", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "calculateCollateralValue", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "collateralBalanceOfUnderlying", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "collateralOnlyDeposits", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "debtBalanceOfUnderlying", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "depositAPY", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" }, - { "internalType": "uint256", "name": "_timestamp", "type": "uint256" } - ], - "name": "getBorrowAmount", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" }, - { "internalType": "uint256", "name": "_timestamp", "type": "uint256" } - ], - "name": "getDepositAmount", - "outputs": [ - { - "internalType": "uint256", - "name": "totalUserDeposits", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "getModel", - "outputs": [ - { - "internalType": "contract IInterestRateModel", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "getUserLTV", - "outputs": [ - { "internalType": "uint256", "name": "userLTV", "type": "uint256" } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "getUserLiquidationThreshold", - "outputs": [ - { - "internalType": "uint256", - "name": "liquidationThreshold", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "getUserMaximumLTV", - "outputs": [ - { "internalType": "uint256", "name": "maximumLTV", "type": "uint256" } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "getUtilization", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "hasPosition", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "inDebt", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_user", "type": "address" } - ], - "name": "isSolvent", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "lensPing", - "outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "liquidity", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "protocolFees", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "siloRepository", - "outputs": [ - { - "internalType": "contract ISiloRepository", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "totalBorrowAmount", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "totalBorrowAmountWithInterest", - "outputs": [ - { - "internalType": "uint256", - "name": "_totalBorrowAmount", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "totalBorrowShare", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "totalDeposits", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "contract ISilo", "name": "_silo", "type": "address" }, - { "internalType": "address", "name": "_asset", "type": "address" } - ], - "name": "totalDepositsWithInterest", - "outputs": [ - { "internalType": "uint256", "name": "_totalDeposits", "type": "uint256" } - ], - "stateMutability": "view", - "type": "function" - } -] diff --git a/silo/main.py b/silo/main.py deleted file mode 100644 index 1627da4b..00000000 --- a/silo/main.py +++ /dev/null @@ -1,129 +0,0 @@ -import os - -import requests -from dotenv import load_dotenv - -from utils.http import request_with_retry -from utils.logging import get_logger -from utils.telegram import send_telegram_message - -load_dotenv() -api_key = os.getenv("GRAPH_API_KEY") -PROTOCOL = "silo" -logger = get_logger(PROTOCOL) - - -def check_positions(): - first = 100 # Number of items to fetch per request - skip = 0 # Start with the first set of results - - # Silo ID's to monitor - silo_ids = [ - "0xea9961280b48fe521ece83f6cd8a7e9b2c4ffc2e", # PENDLE, there is bad debt so here for test purposes - "0x7bec832FF8060cD396645Ccd51E9E9B0E5d8c6e4", # weETH - "0x4a2bd8dcc2539e19cb97DF98EF5afC4d069d9e4C", # ezETH - "0x69eC552BE56E6505703f0C861c40039e5702037A", # WBTC - "0xA8897b4552c075e884BDB8e7b704eB10DB29BF0D", # wstETH - "0x601B76d37a2e06E971d3D63Cf16f41A44E306013", # uniETH - "0x0696E6808EE11a5750733a3d821F9bB847E584FB", # ARB - # add here - ] - silo_ids_string = ",".join([f'"{silo_id}"' for silo_id in silo_ids]) - - while True: - query = f""" - query QueryPositions {{ - siloPositions( - first: {first}, - skip: {skip}, - where: {{ - silo_: {{id_in: [{silo_ids_string}]}}, - riskFactor_gt: 0.9, # >1.0 means insolvent, very close to this value would mean "about to be liquidated" - riskScore_gt: 50000, # 50K is usually around 50k$ so a good value, imo - totalBorrowValue_gt: 0 - }}, - orderBy: riskFactor, - orderDirection: desc, - ) {{ - account {{ - id - }} - silo {{ - id - name - marketAssets: market {{ - inputToken {{ - symbol - }} - }} - }} - totalBorrowValue - riskFactor - riskScore - }} - }} - """ - - json_data = { - "query": query, - "operationName": "QueryPositions", - } - - try: - response = request_with_retry( - "post", - f"https://gateway-arbitrum.network.thegraph.com/api/{api_key}/subgraphs/id/2ufoztRpybsgogPVW6j9NTn1JmBWFYPKbP7pAabizADU", - json=json_data, - ) - except requests.RequestException as e: - logger.error("Graph API query failed after retries: %s", e) - send_telegram_message(f"Graph API query failed after retries: {e}", PROTOCOL, True) - return - - response_data = response.json() - if "errors" in response_data: - logger.error("GraphQL error in response: %s", response_data["errors"]) - send_telegram_message(f"GraphQL error in response: {response_data['errors']}", PROTOCOL, True) - return - - # Check if there are any positions returned - positions = response_data["data"]["siloPositions"] - if not positions: - break - - # Process each position - for position in positions: - wallet_address = position["account"]["id"] - input_token_symbol = position["silo"]["marketAssets"][0]["inputToken"]["symbol"] - silo_name = position["silo"]["name"] - silo_id = position["silo"]["id"] - risk_factor = position["riskFactor"] - risk_score = position["riskScore"] - total_borrow_value = position["totalBorrowValue"] - - message = f""" - High Risk Position Detected! - Wallet Address: {wallet_address} - Input Token Symbol: {input_token_symbol} - Silo Name: {silo_name} - Silo ID: {silo_id} - Risk Factor: {risk_factor} - Risk Score: {risk_score} - Total Borrow Value: {total_borrow_value} - """ - disable_notification = True - if float(risk_factor) > 1: - disable_notification = False - send_telegram_message(message, PROTOCOL, disable_notification) - - # Increment the skip value to fetch the next set of results - skip += first - - -def main(): - logger.info("Checking positions in Arbitrum") - check_positions() - - -if __name__ == "__main__": - main() diff --git a/silo/ur_sniff.py b/silo/ur_sniff.py deleted file mode 100644 index 298566e5..00000000 --- a/silo/ur_sniff.py +++ /dev/null @@ -1,59 +0,0 @@ -from utils.abi import load_abi -from utils.alert import Alert, AlertSeverity, send_alert -from utils.chains import Chain -from utils.logging import get_logger -from utils.web3_wrapper import ChainManager - -THRESHOLD_UR = 0.98 -PROTOCOL = "silo" -logger = get_logger(PROTOCOL) - -# Define addresses by chain (following aave pattern) -ADDRESSES_BY_CHAIN = { - Chain.ARBITRUM: { - "lens": "0xBDb843c7a7e48Dc543424474d7Aa63b61B5D9536", - "usdc_e": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", - "silos": [ - ["Silo WSTETH-USDC.e", "0xA8897b4552c075e884BDB8e7b704eB10DB29BF0D"], - ["Silo WBTC-USDC.e", "0x69eC552BE56E6505703f0C861c40039e5702037A"], - ["Silo ARB-USDC.E", "0x0696E6808EE11a5750733a3d821F9bB847E584FB"], - ], - } -} - -# Load ABI once -ABI_SILO_LENS = load_abi("silo/abi/SiloLens.json") - - -def print_stuff(chain_name, token_name, ur): - if ur > THRESHOLD_UR: - message = f"**BEEP BOP**\n💎 Market asset: {token_name}\n📊 Utilization rate: {ur:.2%}\n🌐 Chain: {chain_name}" - send_alert(Alert(AlertSeverity.LOW, message, PROTOCOL)) - - -def process_assets(chain: Chain): - chain_data = ADDRESSES_BY_CHAIN[chain] - client = ChainManager.get_client(chain) - silo_lens = client.eth.contract(address=chain_data["lens"], abi=ABI_SILO_LENS) - - with client.batch_requests() as batch: - for silo_name, silo_address in chain_data["silos"]: - batch.add(silo_lens.functions.getUtilization(silo_address, chain_data["usdc_e"])) - - responses = client.execute_batch(batch) - if len(responses) != len(chain_data["silos"]): - raise ValueError(f"Expected {len(chain_data['silos'])} responses from batch, got: {len(responses)}") - - for (silo_name, _), ur in zip(chain_data["silos"], responses): - human_readable_ur = ur / 1e18 - print_stuff(chain.name, silo_name, human_readable_ur) - - -def main(): - for chain in [Chain.ARBITRUM]: - logger.info("Processing %s Silos...", chain.name) - process_assets(chain) - - -if __name__ == "__main__": - main() diff --git a/timelock/README.md b/timelock/README.md index 415d4c1a..290eb531 100644 --- a/timelock/README.md +++ b/timelock/README.md @@ -1,6 +1,6 @@ # Timelock Monitoring -Monitors all timelock contract types (TimelockController, Aave, Compound, Puffer, Lido, Maple) and sends Telegram alerts to protocol-specific channels. +Monitors all timelock contract types (TimelockController, Aave, Compound, Lido, Maple) and sends Telegram alerts to protocol-specific channels. ## How It Works @@ -13,7 +13,7 @@ The script runs [hourly via GitHub Actions](../.github/workflows/hourly.yml). ## GraphQL Schema -The script queries the unified `TimelockEvent` type from the Envio indexer. The query fetches all timelock types (TimelockController, Aave, Compound, Puffer, Lido, Maple) for monitored addresses. +The script queries the unified `TimelockEvent` type from the Envio indexer. The query fetches all timelock types (TimelockController, Aave, Compound, Lido, Maple) for monitored addresses. ### Query Structure @@ -58,7 +58,7 @@ The `TimelockEvent` type includes fields that vary by timelock type: **Common fields (all types):** - **`id`** - Unique identifier: `${chainId}_${blockNumber}_${logIndex}` - **`timelockAddress`** - Address of the timelock contract -- **`timelockType`** - Type discriminator: `"TimelockController"`, `"Aave"`, `"Compound"`, `"Puffer"`, `"Lido"`, or `"Maple"` +- **`timelockType`** - Type discriminator: `"TimelockController"`, `"Aave"`, `"Compound"`, `"Lido"`, or `"Maple"` - **`eventName`** - Original event name (e.g., `"CallScheduled"`, `"ProposalQueued"`, `"QueueTransaction"`, etc.) - **`chainId`** - Chain ID (1 for Mainnet, 8453 for Base, etc.) - **`blockNumber`** - Block number where the event was emitted @@ -70,7 +70,6 @@ The `TimelockEvent` type includes fields that vary by timelock type: - **TimelockController**: `target`, `value`, `data`, `delay` (relative seconds), `predecessor`, `index` - **Aave**: `votesFor`, `votesAgainst`, `operationId` (proposalId) - **Compound**: `target`, `value`, `data`, `delay` (absolute timestamp/eta), `signature`, `operationId` (txHash) -- **Puffer**: `target`, `data`, `delay` (absolute timestamp/lockedUntil), `operationId` (txHash) - **Lido**: `creator`, `metadata`, `operationId` (voteId) - **Maple**: `delay` (absolute timestamp/delayedUntil), `operationId` (proposalId) @@ -83,8 +82,6 @@ For complete field mapping details, see [`detils.md`](./detils.md). | [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 | -| [0xe1f03b7b0ebf84e9b9f62a1db40f1efb8faa7d22](https://etherscan.io/address/0xe1f03b7b0ebf84e9b9f62a1db40f1efb8faa7d22) | Mainnet | SILO | Silo TimelockController | -| [0x81f6e9914136da1a1d3b1efd14f7e0761c3d4cc7](https://etherscan.io/address/0x81f6e9914136da1a1d3b1efd14f7e0761c3d4cc7) | Mainnet | LRT | Renzo(ezETH) TimelockController | | [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 | @@ -92,7 +89,6 @@ For complete field mapping details, see [`detils.md`](./detils.md). | [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 | -| [0x3c28b7c7ba1a1f55c9ce66b263b33b204f2126ea](https://etherscan.io/address/0x3c28b7c7ba1a1f55c9ce66b263b33b204f2126ea) | Mainnet | LRT | Puffer 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 TimelockController | @@ -127,7 +123,7 @@ Parameters: The alert format varies by timelock type: -**TimelockController/Compound/Puffer:** +**TimelockController/Compound:** ``` ⏰ TIMELOCK: New Operation Scheduled 🅿️ Protocol: LRT @@ -191,4 +187,4 @@ The script stores the latest processed `blockTimestamp` in `cache-id.txt` under ## Schema Details -For comprehensive information about the unified `TimelockEvent` schema, including field mappings for all supported timelock types (TimelockController, Aave, Compound, Puffer, Lido, Maple), see [`detils.md`](./detils.md). +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). diff --git a/timelock/timelock_alerts.py b/timelock/timelock_alerts.py index 42c7e833..da22b24f 100644 --- a/timelock/timelock_alerts.py +++ b/timelock/timelock_alerts.py @@ -44,8 +44,6 @@ class TimelockConfig: TimelockConfig("0xd8236031d8279d82e615af2bfab5fc0127a329ab", 1, "CAP", "CAP TimelockController"), TimelockConfig("0x5d8a7dc9405f08f14541ba918c1bf7eb2dace556", 1, "RTOKEN", "ETH+ Timelock"), TimelockConfig("0x055e84e7fe8955e2781010b866f10ef6e1e77e59", 1, "LRT", "Lombard TimeLock"), - TimelockConfig("0xe1f03b7b0ebf84e9b9f62a1db40f1efb8faa7d22", 1, "SILO", "Silo TimelockController"), - TimelockConfig("0x81f6e9914136da1a1d3b1efd14f7e0761c3d4cc7", 1, "LRT", "Renzo(ezETH) TimelockController"), TimelockConfig("0x9f26d4c958fd811a1f59b01b86be7dffc9d20761", 1, "LRT", "EtherFi Timelock"), TimelockConfig("0x49bd9989e31ad35b0a62c20be86335196a3135b1", 1, "LRT", "KelpDAO(rsETH) Timelock"), TimelockConfig("0x3d18480cc32b6ab3b833dcabd80e76cfd41c48a9", 1, "INFINIFI", "Infinifi Longtimelock"), @@ -53,7 +51,6 @@ class TimelockConfig: TimelockConfig("0x9aee0b04504cef83a65ac3f0e838d0593bcb2bc7", 1, "AAVE", "Aave Governance V3"), TimelockConfig("0x6d903f6003cca6255d85cca4d3b5e5146dc33925", 1, "COMP", "Compound Timelock"), TimelockConfig("0x2386dc45added673317ef068992f19421b481f4c", 1, "FLUID", "Fluid Timelock"), - TimelockConfig("0x3c28b7c7ba1a1f55c9ce66b263b33b204f2126ea", 1, "LRT", "Puffer Timelock"), TimelockConfig("0x2e59a20f205bb85a89c53f1936454680651e618e", 1, "LIDO", "Lido Timelock"), TimelockConfig("0x2efff88747eb5a3ff00d4d8d0f0800e306c0426b", 1, "MAPLE", "Maple GovernorTimelock"), TimelockConfig("0xb2a3cf69c97afd4de7882e5fee120e4efc77b706", 1, "STRATA", "Strata 48h Timelock"), @@ -196,7 +193,7 @@ def _format_delay_info(delay: int | None, timelock_type: str) -> str | None: return None delay_val = int(delay) - if timelock_type in ("Compound", "Puffer", "Maple"): + if timelock_type in ("Compound", "Maple"): # Absolute timestamp relative = delay_val - int(time.time()) if relative > 0: @@ -207,7 +204,7 @@ def _format_delay_info(delay: int | None, timelock_type: str) -> str | None: def _build_call_info(event: dict, explorer: str | None, show_index: bool, chain_id: int = 0) -> list[str]: - """Build call info lines for TimelockController/Compound/Puffer events.""" + """Build call info lines for TimelockController/Compound events.""" lines: list[str] = [] target = event.get("target") if not target: @@ -245,7 +242,6 @@ def _build_call_info(event: dict, explorer: str | None, show_index: bool, chain_ else: lines.append(f"🔄 New impl: `{new_impl}`") - # Value only for types that have it (not Puffer) value = event.get("value") if value and int(value) > 0: lines.append(f"💰 Value: {int(value) / 1e18:.4f} ETH") @@ -341,7 +337,7 @@ def build_alert_message(events: list[dict], timelock_info: TimelockConfig) -> st elif timelock_type == "Maple": call_lines.append(f"🆔 Proposal: {first.get('operationId') or ''}") - elif timelock_type in ("TimelockController", "Compound", "Puffer"): + elif timelock_type in ("TimelockController", "Compound"): for event in events: call_lines.extend(_build_call_info(event, explorer, len(events) > 1, chain_id)) diff --git a/usd0/README.md b/usd0/README.md deleted file mode 100644 index ad5bed0d..00000000 --- a/usd0/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# USD0 - Usual Money - -**Monitoring - DISABLED** - -[Usual](https://usual.money/) is team behind stablecoin USD0. For more info check [this post by Llama Risk](https://www.llamarisk.com/research/pegkeeper-onboarding-usd0). - -## Governance - -Protocol governance is managed through a [8/15 multisig without timelock](https://etherscan.io/address/0x6e9d65eC80D69b1f508560Bc7aeA5003db1f7FB7). The team intends to add timelocks to their multisigs when the associated functions and roles are significant. All smart contracts are verified onchain and upgradeable through protocol governance mechanisms. - -We are monitoring [multisig address](safe/main.py#162) for new queued transactions. - -TODO: think about monitoring pause role multisigs, like newly added [proxy](https://etherscan.io/address/0x30f1A5916b93ac55AE222EbA9d5a7B0aBb0Ab49A). Here is the [list of roles](https://vscode.blockscan.com/ethereum/0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5). - -## Collateral Factor - -Usual maintains an insurance fund mechanism as part of its protocol treasury rather than utilizing a separate vault structure. The fund serves as a protective buffer against extreme market events and temporary collateral value fluctuations. The Protocol aims to maintain a minimum 30 basis points (bps) protective buffer at all times to ensure collateral security. - -Running daily script to check the collateral factor value. - -## USD0 Price Peg - -Running hourly script to check the peg of USD0. It uses Curve pool USD0/USDC to check the peg. - -## Treasury - -TODO: we could monitor treasury contract: https://etherscan.io/address/0xdd82875f0840AAD58a455A70B88eEd9F59ceC7c7 diff --git a/usd0/main.py b/usd0/main.py deleted file mode 100644 index 852669e7..00000000 --- a/usd0/main.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -from dotenv import load_dotenv -from dune_client.client import DuneClient - -from utils.telegram import send_telegram_message - -load_dotenv() -dune = DuneClient(os.getenv("DUNE_API_KEY")) -PROTOCOL = "usd0" -COLLATERAL_FACTOR_MINIMUM = 100.6 - - -def query_cf(): - query_result = dune.get_latest_result(3886520) - newest_data = query_result.result.rows[0] - collateral_factor = newest_data["collateral_factor"] - if collateral_factor < COLLATERAL_FACTOR_MINIMUM: - # Collateral factor has fallen below accept risk - message = f"USD0 collateral factor is {collateral_factor}" - send_telegram_message(message, PROTOCOL) - - -if __name__ == "__main__": - query_cf() diff --git a/usd0/price.py b/usd0/price.py deleted file mode 100644 index a0f37dba..00000000 --- a/usd0/price.py +++ /dev/null @@ -1,63 +0,0 @@ -import json - -from utils.chains import Chain -from utils.telegram import send_telegram_message -from utils.web3_wrapper import ChainManager - -PROTOCOL = "usd0" -peg_threshold = 0.001 # 0.1% used - -# Load ABI -with open("common-abi/CurvePool.json") as f: - abi_data = json.load(f) - abi_curve_pool = abi_data["result"] if isinstance(abi_data, dict) else abi_data - - -def check_peg(usdc_rate, curve_rate): - if curve_rate == 0: - return False - difference = abs(usdc_rate - curve_rate) - percentage_diff = difference / usdc_rate - return percentage_diff >= peg_threshold - - -def check_peg_usd0(): - amounts = [100_000e18, 1_000_000e18, 10_000_000e18] - - # Get Web3 client for mainnet - client = ChainManager.get_client(Chain.MAINNET) - - # Initialize curve pool contract - curve_pool = client.eth.contract(address="0x14100f81e33C33Ecc7CDac70181Fb45B6E78569F", abi=abi_curve_pool) - - # Create batch request - batch = client.batch_requests() - - # Add all get_dy calls to the batch - calls = [(amount, batch.add(curve_pool.functions.get_dy(0, 1, int(amount)))) for amount in amounts] - - try: - # Execute all calls at once - responses = batch.execute() - if len(responses) != len(amounts): - raise ValueError(f"Expected {len(amounts)} responses from batch, got: {len(responses)}") - - # Process results - message = "" - for (amount, _), curve_rate in zip(calls, responses): - if curve_rate is not None and check_peg(amount / 1e12, curve_rate): - human_readable_amount = amount / 1e18 - human_readable_result = curve_rate / 1e6 - message += f"📊 Swap result: {human_readable_amount:,.2f} USD0 -> {human_readable_result:,.2f} USDC\n" - - if len(message) > 0: - send_telegram_message(message, PROTOCOL) - - except Exception as e: - error_message = f"Error executing batch requests: {e}" - send_telegram_message(error_message, PROTOCOL) - return - - -if __name__ == "__main__": - check_peg_usd0() diff --git a/utils/tenderly/alerts_with_timelock.json b/utils/tenderly/alerts_with_timelock.json index 024f8403..3d308ae9 100644 --- a/utils/tenderly/alerts_with_timelock.json +++ b/utils/tenderly/alerts_with_timelock.json @@ -300,122 +300,6 @@ } ] }, - { - "id": "363c626b-2249-4d51-bad8-40cc92b58f24", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in Silo TimelockController", - "description": "New transaction is scheduled for Silo Timelock", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-08-23T10:02:46.889821Z", - "delivery_channels": [ - { - "delivery_channel_id": "9f8c54f0-78a0-494f-8028-c87c79afd297", - "delivery_channel": { - "id": "9f8c54f0-78a0-494f-8028-c87c79afd297", - "type": "telegram", - "owner_id": "1f750734-c720-4fde-9d60-d7edd0c04d91", - "project_id": null, - "label": "Silo Alerts SAM", - "reference_id": "2c8727c0-2440-4fbb-bf97-af7354b2081e", - "enabled": true, - "created_at": "2024-08-10T23:32:27.651517Z", - "information": { - "chat_id": -1002206264426 - } - }, - "enabled": true, - "created_at": "2024-09-02T12:15:56.017182Z" - } - ], - "is_editable": true, - "updated_at": "2024-09-02T12:15:56.024732Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0xe1f03b7b0ebf84e9b9f62a1db40f1efb8faa7d22", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0xe1f03b7b0ebf84e9b9f62a1db40f1efb8faa7d22", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, - { - "id": "65153e56-1f79-45a2-8453-b61beeeab411", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Event CallScheduled emitted in Renzo(ezETH) TimelockController", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2024-10-13T21:45:48.149499Z", - "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-04-02T14:58:55.247418Z" - } - ], - "is_editable": true, - "updated_at": "2025-04-02T14:58:55.249021Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x81f6e9914136da1a1d3b1efd14f7e0761c3d4cc7", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "emitted_log", - "expression": { - "address": "0x81f6e9914136da1a1d3b1efd14f7e0761c3d4cc7", - "match_any": false, - "match_non_project_contracts": false, - "event_name": "CallScheduled", - "event_id": "0x4cf4410cc57040e44862ef0f45f3dd5a5e02db8eb8add648d4b0e236f1d07dca", - "decode_events": false - } - } - ] - }, { "id": "6c4b81c9-3130-4b3c-9356-15c9abc7b918", "project_id": "6932cbec-2970-425a-a734-cb8374d95744", @@ -899,68 +783,6 @@ } ] }, - { - "id": "f6654146-08d0-4a83-917a-23233be2314e", - "project_id": "6932cbec-2970-425a-a734-cb8374d95744", - "name": "Function queueTransaction called in pufETH timelock", - "description": "", - "enabled": true, - "color": "#eeeeee", - "severity": "default", - "created_at": "2025-04-28T17:26:57.480318Z", - "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-04-28T17:26:57.563249Z" - } - ], - "is_editable": true, - "updated_at": "2025-04-28T17:26:57.480318Z", - "expressions": [ - { - "type": "contract_address", - "expression": { - "address": "0x3c28b7c7ba1a1f55c9ce66b263b33b204f2126ea", - "transaction_type": null - } - }, - { - "type": "network", - "expression": { - "network_id": "1" - } - }, - { - "type": "method_call", - "expression": { - "address": "0x3c28b7c7ba1a1f55c9ce66b263b33b204f2126ea", - "function_name": "queueTransaction", - "signature": { - "function_name": "queueTransaction", - "input_types": [ - "address", - "bytes", - "uint256" - ] - } - } - } - ] - }, { "id": "fba03d32-4920-41c0-9e4c-8c007e8b000e", "project_id": "6932cbec-2970-425a-a734-cb8374d95744",