From cfd8e296b9ba9aeb9b55fdf1dfe51872e3b46eb7 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 13 Jun 2025 20:48:26 +0200 Subject: [PATCH 1/8] feat: inverse finance monitoring --- .github/workflows/hourly.yml | 2 + inverse/README.md | 20 +++++ inverse/inverse.py | 138 +++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 inverse/README.md create mode 100644 inverse/inverse.py diff --git a/.github/workflows/hourly.yml b/.github/workflows/hourly.yml index 5c66ed1..d554948 100644 --- a/.github/workflows/hourly.yml +++ b/.github/workflows/hourly.yml @@ -117,6 +117,8 @@ jobs: run: uv run rtoken/monitor_rtoken.py # - name: Run USD0 Peg Price Checker # run: uv run usd0/price.py + - name: Run Inverse script + run: uv run inverse/inverse.py # Check final hash - name: Check final hash diff --git a/inverse/README.md b/inverse/README.md new file mode 100644 index 0000000..6cc50ae --- /dev/null +++ b/inverse/README.md @@ -0,0 +1,20 @@ +# Inverse Finance + +## FiRM (Fixed Rate Market) + +### Data Monitoring + +The script `inverse/inverse.py` runs [hourly via GitHub Actions](../.github/workflows/hourly.yml) to monitor key health indicators of the Inverse Finance system using API data. + +#### FiRM Monitoring + +- **DOLA Supply Check**: Alerts if FiRM's circulating supply exceeds the total DOLA circulating supply by more than 9M DOLA (accounting for Frontier bad debt and Gearbox). Threshold defined [in code](inverse.py#L113). +- **TVL to Borrows Ratio**: Alerts if borrows exceed 80% of the TVL, indicating high utilization. Threshold defined [in code](inverse.py#L116). + +#### DOLA Staking Monitoring + +- **DOLA Price Stability**: Alerts if DOLA price drops below $0.998, indicating potential depegging. Threshold defined [in code](inverse.py#L123). +- **Staking Coverage**: Alerts if total staked DOLA assets are less than the sDOLA supply, indicating potential undercollateralization. Check defined [in code](inverse.py#L126). +- **Exchange Rate Validation**: Verifies that the calculated exchange rate matches the reported rate from the API. Check defined [in code](inverse.py#L129). + +All API responses are validated to ensure data is not older than 2 hours. Timestamp validation logic defined [in code](inverse.py#L39). diff --git a/inverse/inverse.py b/inverse/inverse.py new file mode 100644 index 0000000..9d4f6bb --- /dev/null +++ b/inverse/inverse.py @@ -0,0 +1,138 @@ +import requests +from dataclasses import dataclass +from utils.telegram import send_telegram_message +import time +from datetime import datetime +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +PROTOCOL_NAME = "INVERSE" +INVERSE_API_URL = "https://www.inverse.finance/api" +FED_OVERVIEW_URL = INVERSE_API_URL + "/transparency/fed-overview" +DOLA_CIRCULATING_URL = INVERSE_API_URL + "/dola/circulating-supply" +DOLA_STAKING_URL = INVERSE_API_URL + "/dola-staking" +GOVERNANCE_URL = INVERSE_API_URL + "/governance-notifs" + +@dataclass +class FedOverview: + circSupply: float + protocol: str + name: str + projectImage: str + supply: float + tvl: float + borrows: float + +@dataclass +class Dolastaking: + dola_price_usd: float + tvl_usd: float + total_assets: float + total_assets30d: float + total_assets90d: float + s_dola_ex_rate: float + s_dola_supply: float + s_dola_total_assets: float + + +def is_timestamp_recent(timestamp_ms: int, max_age_hours: int = 2) -> bool: + current_time_ms = int(time.time() * 1000) + max_age_ms = max_age_hours * 60 * 60 * 1000 + return (current_time_ms - timestamp_ms) <= max_age_ms + + +def get_fed_overview() -> FedOverview: + session = create_session_with_retry() + response = session.get(FED_OVERVIEW_URL) + if response.status_code != 200: + raise Exception(f"Failed to get fed overview: {response.status_code}") + + timestamp = response.json()["timestamp"] + if not is_timestamp_recent(timestamp): + raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp/1000)}") + + feds_overview = response.json()["fedOverviews"] + firm = feds_overview[0] + if firm["protocol"] != "FiRM": + raise Exception("FiRM is not the first protocol in the list") + return FedOverview( + circSupply=firm["circSupply"], + protocol=firm["protocol"], + name=firm["name"], + projectImage=firm["projectImage"], + supply=firm["supply"], + tvl=firm["tvl"], + borrows=firm["borrows"] + ) + +def get_dola_circulating_supply(): + response = requests.get(DOLA_CIRCULATING_URL) + if response.status_code != 200: + raise Exception(f"Failed to get dola circulating supply: {response.status_code}") + return response.json() + +def create_session_with_retry( + retries: int = 3, + backoff_factor: float = 0.5, + status_forcelist: tuple = (500, 502, 504) +) -> requests.Session: + session = requests.Session() + retry = Retry( + total=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + +def get_dola_staking() -> Dolastaking: + session = create_session_with_retry() + try: + response = session.get(DOLA_STAKING_URL, timeout=10) + response.raise_for_status() + + data = response.json() + timestamp = data["timestamp"] + if not is_timestamp_recent(timestamp): + raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp/1000)}") + + return Dolastaking( + dola_price_usd=data["dolaPriceUsd"], + tvl_usd=data["tvlUsd"], + total_assets=data["totalAssets"], + total_assets30d=data["totalAssets30d"], + total_assets90d=data["totalAssets90d"], + s_dola_ex_rate=data["sDolaExRate"], + s_dola_supply=data["sDolaSupply"], + s_dola_total_assets=data["sDolaTotalAssets"], + ) + except requests.exceptions.RequestException as e: + raise Exception(f"Failed to get dola staking after retries: {str(e)}") + finally: + session.close() + + +if __name__ == "__main__": + fed_overview = get_fed_overview() + dola_circulating_supply = get_dola_circulating_supply() + # NOTE: there is a 9M DOLA from frontier(bad debt) and gearbox + if fed_overview.circSupply > dola_circulating_supply + 9e6: + send_telegram_message(f"🚨 FiRM has {fed_overview.circSupply - dola_circulating_supply - 9e6} more DOLA than the circulating supply", PROTOCOL_NAME) + + if fed_overview.tvl * 0.8 < fed_overview.borrows: + send_telegram_message(f"🚨 Borrows exceed 80% of the TVL. FiRM has TVL: {fed_overview.tvl} and borrows: {fed_overview.borrows}.", PROTOCOL_NAME) + + # other metrics like liquidity should be handled in liquidity repository + + dola_staking = get_dola_staking() + if dola_staking.dola_price_usd < 0.998: + send_telegram_message(f"🚨 DOLA price is below $0.998. DOLA price: {dola_staking.dola_price_usd}", PROTOCOL_NAME) + + if dola_staking.s_dola_total_assets < dola_staking.s_dola_supply: + send_telegram_message(f"🚨 DOLA staking is not enough. DOLA staking: {dola_staking.s_dola_total_assets} and DOLA supply: {dola_staking.s_dola_supply}", PROTOCOL_NAME) + + ex_rate = dola_staking.s_dola_total_assets / dola_staking.s_dola_supply + if ex_rate != dola_staking.s_dola_ex_rate: + send_telegram_message(f"🚨 DOLA ex rate is not correct, calculated: {ex_rate} and target: {dola_staking.s_dola_ex_rate}", PROTOCOL_NAME) From 35ec37c0e9bda725070d4778fccd5d2bfbeb894e Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 13 Jun 2025 21:09:25 +0200 Subject: [PATCH 2/8] feat: verify sdola supply --- inverse/README.md | 4 ++++ inverse/abi/sdola.json | 15 +++++++++++++++ inverse/inverse.py | 34 +++++++++++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 inverse/abi/sdola.json diff --git a/inverse/README.md b/inverse/README.md index 6cc50ae..29b911d 100644 --- a/inverse/README.md +++ b/inverse/README.md @@ -18,3 +18,7 @@ The script `inverse/inverse.py` runs [hourly via GitHub Actions](../.github/work - **Exchange Rate Validation**: Verifies that the calculated exchange rate matches the reported rate from the API. Check defined [in code](inverse.py#L129). All API responses are validated to ensure data is not older than 2 hours. Timestamp validation logic defined [in code](inverse.py#L39). + +#### SDOLA Supply Monitoring + +Check SDOLA supply from the contract and verify it matches the supply from the API. diff --git a/inverse/abi/sdola.json b/inverse/abi/sdola.json new file mode 100644 index 0000000..1535ef2 --- /dev/null +++ b/inverse/abi/sdola.json @@ -0,0 +1,15 @@ +[ + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/inverse/inverse.py b/inverse/inverse.py index 9d4f6bb..a16b218 100644 --- a/inverse/inverse.py +++ b/inverse/inverse.py @@ -1,17 +1,36 @@ -import requests -from dataclasses import dataclass -from utils.telegram import send_telegram_message +import json import time +from dataclasses import dataclass from datetime import datetime + +import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry +from utils.chains import Chain +from utils.telegram import send_telegram_message +from utils.web3_wrapper import ChainManager + PROTOCOL_NAME = "INVERSE" INVERSE_API_URL = "https://www.inverse.finance/api" FED_OVERVIEW_URL = INVERSE_API_URL + "/transparency/fed-overview" DOLA_CIRCULATING_URL = INVERSE_API_URL + "/dola/circulating-supply" DOLA_STAKING_URL = INVERSE_API_URL + "/dola-staking" GOVERNANCE_URL = INVERSE_API_URL + "/governance-notifs" +SDOLA_CONTRACT = "0xb45ad160634c528Cc3D2926d9807104FA3157305" + +# TODO: remove and use utils.abi.get_abi +def load_abi(file_path): + with open(file_path) as f: + abi_data = json.load(f) + if isinstance(abi_data, dict): + return abi_data["result"] + elif isinstance(abi_data, list): + return abi_data + else: + raise ValueError("Invalid ABI format") + +SDOLA_ABI = load_abi("inverse/abi/sdola.json") @dataclass class FedOverview: @@ -113,6 +132,10 @@ def get_dola_staking() -> Dolastaking: finally: session.close() +def get_sdola_supply() -> int: + client = ChainManager.get_client(Chain.MAINNET) + contract = client.eth.contract(address=SDOLA_CONTRACT, abi=SDOLA_ABI) + return contract.functions.totalSupply().call() if __name__ == "__main__": fed_overview = get_fed_overview() @@ -136,3 +159,8 @@ def get_dola_staking() -> Dolastaking: ex_rate = dola_staking.s_dola_total_assets / dola_staking.s_dola_supply if ex_rate != dola_staking.s_dola_ex_rate: send_telegram_message(f"🚨 DOLA ex rate is not correct, calculated: {ex_rate} and target: {dola_staking.s_dola_ex_rate}", PROTOCOL_NAME) + + # Verify backend is returning correct supply + sdola_supply = get_sdola_supply() / 1e18 + if sdola_supply < dola_staking.s_dola_supply * 0.99 or sdola_supply > dola_staking.s_dola_supply * 1.01: + send_telegram_message(f"🚨 SDOLA supply is not correct, calculated: {sdola_supply} and target: {dola_staking.s_dola_supply}", PROTOCOL_NAME) From 5a92bf42421f3bdbfedf1c42e7fc3b702232230d Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 20 Jun 2025 16:16:56 +0200 Subject: [PATCH 3/8] feat: inverse additional monitoring --- inverse/inverse.py | 260 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 217 insertions(+), 43 deletions(-) diff --git a/inverse/inverse.py b/inverse/inverse.py index 56a86b7..33e16c9 100644 --- a/inverse/inverse.py +++ b/inverse/inverse.py @@ -1,6 +1,8 @@ import time from dataclasses import dataclass from datetime import datetime +from enum import Enum +from typing import Dict, List, Optional import requests from requests.adapters import HTTPAdapter @@ -20,18 +22,27 @@ SDOLA_CONTRACT = "0xb45ad160634c528Cc3D2926d9807104FA3157305" SDOLA_ABI = load_abi("inverse/abi/sdola.json") +class FedType(Enum): + FIRM = "FiRM" + AMM = "AMM" + CROSS_CHAIN = "CROSS_CHAIN" + DEPRECATED = "DEPRECATED" + @dataclass -class FedOverview: +class FedInfo: circSupply: float protocol: str name: str - projectImage: str supply: float tvl: float borrows: float + fed_type: FedType + utilization_ratio: Optional[float] = None + lp_total_supply: Optional[float] = None + lp_price: Optional[float] = None @dataclass -class Dolastaking: +class DolaStaking: dola_price_usd: float tvl_usd: float total_assets: float @@ -41,38 +52,77 @@ class Dolastaking: s_dola_supply: float s_dola_total_assets: float +@dataclass +class FedMonitoringMetrics: + total_fed_supply: float + firm_utilization: float + amm_feds_backing: Dict[str, float] + peg_deviation: float def is_timestamp_recent(timestamp_ms: int, max_age_hours: int = 2) -> bool: current_time_ms = int(time.time() * 1000) max_age_ms = max_age_hours * 60 * 60 * 1000 return (current_time_ms - timestamp_ms) <= max_age_ms +def classify_fed_type(protocol: str, name: str) -> FedType: + """Classify Fed type based on protocol and name""" + if protocol == "FiRM": + return FedType.FIRM + elif protocol in ["Frontier", "Fuse6", "Fuse24"]: + return FedType.DEPRECATED + elif protocol in ["Badger", "0xb1", "Yearn", "Convex", "Scream", "Velo", + "Aura", "AuraEuler", "Aero", "FraxPyusd"]: + return FedType.AMM + elif protocol in ["ArbiFed", "BaseCCTP", "OptiCCTP", "Gearbox"]: + return FedType.CROSS_CHAIN + else: + return FedType.AMM # Default to AMM for unknown protocols -def get_fed_overview() -> FedOverview: +def get_all_feds_overview() -> List[FedInfo]: + """Get comprehensive overview of all Fed contracts""" session = create_session_with_retry() response = session.get(FED_OVERVIEW_URL) if response.status_code != 200: raise Exception(f"Failed to get fed overview: {response.status_code}") - timestamp = response.json()["timestamp"] + data = response.json() + timestamp = data["timestamp"] if not is_timestamp_recent(timestamp): raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp/1000)}") - feds_overview = response.json()["fedOverviews"] - firm = feds_overview[0] - if firm["protocol"] != "FiRM": - raise Exception("FiRM is not the first protocol in the list") - return FedOverview( - circSupply=firm["circSupply"], - protocol=firm["protocol"], - name=firm["name"], - projectImage=firm["projectImage"], - supply=firm["supply"], - tvl=firm["tvl"], - borrows=firm["borrows"] - ) + feds_overview = data["fedOverviews"] + feds = [] + + for fed_data in feds_overview: + fed_type = classify_fed_type(fed_data["protocol"], fed_data["name"]) + + # Safely get values with defaults for missing fields + tvl = fed_data.get("tvl", 0.0) + supply = fed_data.get("supply", 0.0) + borrows = fed_data.get("borrows", 0.0) + circ_supply = fed_data.get("circSupply", 0.0) + + utilization_ratio = None + if fed_type == FedType.FIRM and tvl > 0: + utilization_ratio = borrows / tvl + + fed_info = FedInfo( + circSupply=circ_supply, + protocol=fed_data["protocol"], + name=fed_data["name"], + supply=supply, + tvl=tvl, + borrows=borrows, + fed_type=fed_type, + utilization_ratio=utilization_ratio, + lp_total_supply=fed_data.get("lpTotalSupply", None), + lp_price=fed_data.get("lpPrice", None) + ) + feds.append(fed_info) + + return feds -def get_dola_circulating_supply(): +def get_dola_circulating_supply() -> float: response = requests.get(DOLA_CIRCULATING_URL) if response.status_code != 200: raise Exception(f"Failed to get dola circulating supply: {response.status_code}") @@ -94,7 +144,7 @@ def create_session_with_retry( session.mount('https://', adapter) return session -def get_dola_staking() -> Dolastaking: +def get_dola_staking() -> DolaStaking: session = create_session_with_retry() try: response = session.get(DOLA_STAKING_URL, timeout=10) @@ -105,7 +155,7 @@ def get_dola_staking() -> Dolastaking: if not is_timestamp_recent(timestamp): raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp/1000)}") - return Dolastaking( + return DolaStaking( dola_price_usd=data["dolaPriceUsd"], tvl_usd=data["tvlUsd"], total_assets=data["totalAssets"], @@ -120,35 +170,159 @@ def get_dola_staking() -> Dolastaking: finally: session.close() -def get_sdola_supply() -> int: +def get_sdola_supply() -> float: client = ChainManager.get_client(Chain.MAINNET) contract = client.eth.contract(address=SDOLA_CONTRACT, abi=SDOLA_ABI) - return contract.functions.totalSupply().call() + return contract.functions.totalSupply().call() / 1e18 -if __name__ == "__main__": - fed_overview = get_fed_overview() - dola_circulating_supply = get_dola_circulating_supply() - # NOTE: there is a 9M DOLA from frontier(bad debt) and gearbox - if fed_overview.circSupply > dola_circulating_supply + 9e6: - send_telegram_message(f"🚨 FiRM has {fed_overview.circSupply - dola_circulating_supply - 9e6} more DOLA than the circulating supply", PROTOCOL_NAME) +def calculate_fed_metrics(feds: List[FedInfo], dola_price: float) -> FedMonitoringMetrics: + """Calculate comprehensive Fed monitoring metrics""" + total_fed_supply = sum(fed.supply for fed in feds) + + # FiRM utilization + firm_feds = [fed for fed in feds if fed.fed_type == FedType.FIRM] + firm_utilization = 0.0 + if firm_feds: + firm_fed = firm_feds[0] # Should only be one FiRM Fed + firm_utilization = firm_fed.utilization_ratio or 0.0 - if fed_overview.tvl * 0.8 < fed_overview.borrows: - send_telegram_message(f"🚨 Borrows exceed 80% of the TVL. FiRM has TVL: {fed_overview.tvl} and borrows: {fed_overview.borrows}.", PROTOCOL_NAME) + # AMM Feds backing ratios + amm_feds_backing = {} + for fed in feds: + if fed.fed_type == FedType.AMM and fed.lp_total_supply is not None: + amm_feds_backing[fed.protocol] = fed.lp_total_supply * fed.lp_price - # other metrics like liquidity should be handled in liquidity repository + # Peg stability score (based on DOLA price deviation from $1) + peg_deviation = abs(1.0 - dola_price) - dola_staking = get_dola_staking() - if dola_staking.dola_price_usd < 0.998: - send_telegram_message(f"🚨 DOLA price is below $0.998. DOLA price: {dola_staking.dola_price_usd}", PROTOCOL_NAME) + return FedMonitoringMetrics( + total_fed_supply=total_fed_supply, + firm_utilization=firm_utilization, + amm_feds_backing=amm_feds_backing, + peg_deviation=peg_deviation, + ) +def monitor_firm_fed(fed: FedInfo) -> None: + """Monitor FiRM Fed for risk conditions""" + if fed.fed_type != FedType.FIRM: + return + + # High utilization alert + if fed.utilization_ratio and fed.utilization_ratio > 0.8: + send_telegram_message( + f"🚨 FiRM utilization is high: {fed.utilization_ratio:.1%}. " + f"TVL: ${fed.tvl:,.0f}, Borrows: ${fed.borrows:,.0f}", + PROTOCOL_NAME + ) + + # Collateralization concerns + if fed.supply > 0 and fed.borrows > 0: + collateralization_ratio = fed.supply / fed.borrows + if collateralization_ratio < 1.2: + send_telegram_message( + f"🚨 FiRM collateralization is low: {collateralization_ratio:.1%}. " + f"Supply: ${fed.supply:,.0f}, Borrows: ${fed.borrows:,.0f}", + PROTOCOL_NAME + ) + else: + send_telegram_message( + "Missing supply or borrows for FiRM Fed", + PROTOCOL_NAME + ) + +def monitor_overall_risk(metrics: FedMonitoringMetrics, dola_price: float) -> None: + """Monitor overall system risk""" + if metrics.firm_utilization > 0.8: + send_telegram_message( + f"🚨 FiRM utilization is high: {metrics.firm_utilization:.1%}. " + f"TVL: ${metrics.total_fed_supply:,.0f}, Borrows: ${metrics.total_fed_supply:,.0f}", + PROTOCOL_NAME + ) + # DOLA price alerts + if metrics.peg_deviation > 0.005: + send_telegram_message( + f"🚨 DOLA peg deviation: {metrics.peg_deviation:.4f}. " + f"DOLA price: ${dola_price:.4f}", + PROTOCOL_NAME + ) + + +def monitor_deprecated_feds(feds: List[FedInfo]) -> None: + """Monitor deprecated Feds for unexpected activity""" + deprecated_feds = [fed for fed in feds if fed.fed_type == FedType.DEPRECATED] + + for fed in deprecated_feds: + if fed.protocol == "Frontier": + if fed.supply > 19450000: + send_telegram_message( + f"⚠️ Frontier Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", + PROTOCOL_NAME + ) + elif fed.supply > 1000: + send_telegram_message( + f"⚠️ Deprecated {fed.protocol} Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", + PROTOCOL_NAME + ) + +def monitor_total_fed_supply_borrows(feds: List[FedInfo]) -> None: + """Monitor total Fed supply for unexpected activity""" + total_fed_supply = sum(fed.supply for fed in feds) + total_fed_borrows = sum(fed.borrows for fed in feds) + if total_fed_borrows > total_fed_supply * 0.8: + send_telegram_message( + f"🚨 Total Fed borrows is greater than supply: ${total_fed_borrows:,.0f} > ${total_fed_supply:,.0f} * 0.8", + PROTOCOL_NAME + ) + +def monitor_dola_staking(dola_staking: DolaStaking) -> None: + """Monitor DOLA staking for unexpected activity""" + # DOLA Staking monitoring (existing logic) if dola_staking.s_dola_total_assets < dola_staking.s_dola_supply: - send_telegram_message(f"🚨 DOLA staking is not enough. DOLA staking: {dola_staking.s_dola_total_assets} and DOLA supply: {dola_staking.s_dola_supply}", PROTOCOL_NAME) + send_telegram_message( + f"🚨 sDOLA undercollateralized: Assets {dola_staking.s_dola_total_assets:,.0f} " + f"< Supply {dola_staking.s_dola_supply:,.0f}", + PROTOCOL_NAME + ) + + # Exchange rate validation + calculated_ex_rate = dola_staking.s_dola_total_assets / dola_staking.s_dola_supply + if abs(calculated_ex_rate - dola_staking.s_dola_ex_rate) > 0.001: + send_telegram_message( + f"🚨 sDOLA exchange rate mismatch: Calculated {calculated_ex_rate:.4f} " + f"vs API {dola_staking.s_dola_ex_rate:.4f}", + PROTOCOL_NAME + ) + + # sDOLA supply verification + sdola_supply_onchain = get_sdola_supply() + supply_diff_pct = abs(sdola_supply_onchain - dola_staking.s_dola_supply) / dola_staking.s_dola_supply + if supply_diff_pct > 0.01: # 1% tolerance + send_telegram_message( + f"🚨 sDOLA supply mismatch: On-chain {sdola_supply_onchain:,.0f} " + f"vs API {dola_staking.s_dola_supply:,.0f} ({supply_diff_pct:.1%} diff)", + PROTOCOL_NAME + ) + + +if __name__ == "__main__": + try: + # Get all Fed data + feds = get_all_feds_overview() + dola_staking = get_dola_staking() + metrics = calculate_fed_metrics(feds, dola_staking.dola_price_usd) + + # Monitor each Fed type + for fed in feds: + if fed.fed_type == FedType.FIRM: + monitor_firm_fed(fed) - ex_rate = dola_staking.s_dola_total_assets / dola_staking.s_dola_supply - if ex_rate != dola_staking.s_dola_ex_rate: - send_telegram_message(f"🚨 DOLA ex rate is not correct, calculated: {ex_rate} and target: {dola_staking.s_dola_ex_rate}", PROTOCOL_NAME) + # Monitor overall risk + monitor_overall_risk(metrics, dola_staking.dola_price_usd) + monitor_deprecated_feds(feds) + monitor_total_fed_supply_borrows(feds) + monitor_dola_staking(dola_staking) + print(f"✅ Monitoring completed successfully. Checked {len(feds)} Feds.") - # Verify backend is returning correct supply - sdola_supply = get_sdola_supply() / 1e18 - if sdola_supply < dola_staking.s_dola_supply * 0.99 or sdola_supply > dola_staking.s_dola_supply * 1.01: - send_telegram_message(f"🚨 SDOLA supply is not correct, calculated: {sdola_supply} and target: {dola_staking.s_dola_supply}", PROTOCOL_NAME) + except Exception as e: + print(f"❌ Error during monitoring: {str(e)}") + send_telegram_message(f"🚨 Inverse monitoring error: {str(e)}", PROTOCOL_NAME) From a64e91f234a84212b62ad09e0d873e6ff8e5bb57 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Wed, 25 Jun 2025 14:09:09 +0200 Subject: [PATCH 4/8] feat: dola supply onchain verification --- inverse/README.md | 6 ++- inverse/abi/sdola.json | 15 ------ inverse/inverse.py | 101 +++++++++++++++++++++++++---------------- 3 files changed, 68 insertions(+), 54 deletions(-) delete mode 100644 inverse/abi/sdola.json diff --git a/inverse/README.md b/inverse/README.md index 29b911d..a81f65b 100644 --- a/inverse/README.md +++ b/inverse/README.md @@ -19,6 +19,10 @@ The script `inverse/inverse.py` runs [hourly via GitHub Actions](../.github/work All API responses are validated to ensure data is not older than 2 hours. Timestamp validation logic defined [in code](inverse.py#L39). +#### DOLA Supply Monitoring + +DOLA supply is checked from the contract and verified it matches the supply from the API. API data is calculated by summing all values from FED list. If the difference is greater than 0.5%, telegram alert is sent. + #### SDOLA Supply Monitoring -Check SDOLA supply from the contract and verify it matches the supply from the API. +Check SDOLA supply from the contract and verify it matches the supply from the API, if the difference is greater than 0.5%, telegram alert is sent. diff --git a/inverse/abi/sdola.json b/inverse/abi/sdola.json deleted file mode 100644 index 1535ef2..0000000 --- a/inverse/abi/sdola.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - } -] \ No newline at end of file diff --git a/inverse/inverse.py b/inverse/inverse.py index 33e16c9..cbb7e54 100644 --- a/inverse/inverse.py +++ b/inverse/inverse.py @@ -19,8 +19,10 @@ DOLA_CIRCULATING_URL = INVERSE_API_URL + "/dola/circulating-supply" DOLA_STAKING_URL = INVERSE_API_URL + "/dola-staking" GOVERNANCE_URL = INVERSE_API_URL + "/governance-notifs" +DOLA_CONTRACT = "0x865377367054516e17014CcdED1e7d814EDC9ce4" SDOLA_CONTRACT = "0xb45ad160634c528Cc3D2926d9807104FA3157305" -SDOLA_ABI = load_abi("inverse/abi/sdola.json") +ERC20_ABI = load_abi("common-abi/ERC20.json") + class FedType(Enum): FIRM = "FiRM" @@ -28,6 +30,7 @@ class FedType(Enum): CROSS_CHAIN = "CROSS_CHAIN" DEPRECATED = "DEPRECATED" + @dataclass class FedInfo: circSupply: float @@ -41,6 +44,7 @@ class FedInfo: lp_total_supply: Optional[float] = None lp_price: Optional[float] = None + @dataclass class DolaStaking: dola_price_usd: float @@ -52,6 +56,7 @@ class DolaStaking: s_dola_supply: float s_dola_total_assets: float + @dataclass class FedMonitoringMetrics: total_fed_supply: float @@ -59,25 +64,27 @@ class FedMonitoringMetrics: amm_feds_backing: Dict[str, float] peg_deviation: float + def is_timestamp_recent(timestamp_ms: int, max_age_hours: int = 2) -> bool: current_time_ms = int(time.time() * 1000) max_age_ms = max_age_hours * 60 * 60 * 1000 return (current_time_ms - timestamp_ms) <= max_age_ms -def classify_fed_type(protocol: str, name: str) -> FedType: + +def classify_fed_type(protocol: str) -> FedType: """Classify Fed type based on protocol and name""" if protocol == "FiRM": return FedType.FIRM elif protocol in ["Frontier", "Fuse6", "Fuse24"]: return FedType.DEPRECATED - elif protocol in ["Badger", "0xb1", "Yearn", "Convex", "Scream", "Velo", - "Aura", "AuraEuler", "Aero", "FraxPyusd"]: + elif protocol in ["Badger", "0xb1", "Yearn", "Convex", "Scream", "Velo", "Aura", "AuraEuler", "Aero", "FraxPyusd"]: return FedType.AMM elif protocol in ["ArbiFed", "BaseCCTP", "OptiCCTP", "Gearbox"]: return FedType.CROSS_CHAIN else: return FedType.AMM # Default to AMM for unknown protocols + def get_all_feds_overview() -> List[FedInfo]: """Get comprehensive overview of all Fed contracts""" session = create_session_with_retry() @@ -88,13 +95,13 @@ def get_all_feds_overview() -> List[FedInfo]: data = response.json() timestamp = data["timestamp"] if not is_timestamp_recent(timestamp): - raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp/1000)}") + raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp / 1000)}") feds_overview = data["fedOverviews"] feds = [] for fed_data in feds_overview: - fed_type = classify_fed_type(fed_data["protocol"], fed_data["name"]) + fed_type = classify_fed_type(fed_data["protocol"]) # Safely get values with defaults for missing fields tvl = fed_data.get("tvl", 0.0) @@ -116,22 +123,22 @@ def get_all_feds_overview() -> List[FedInfo]: fed_type=fed_type, utilization_ratio=utilization_ratio, lp_total_supply=fed_data.get("lpTotalSupply", None), - lp_price=fed_data.get("lpPrice", None) + lp_price=fed_data.get("lpPrice", None), ) feds.append(fed_info) return feds + def get_dola_circulating_supply() -> float: response = requests.get(DOLA_CIRCULATING_URL) if response.status_code != 200: raise Exception(f"Failed to get dola circulating supply: {response.status_code}") return response.json() + def create_session_with_retry( - retries: int = 3, - backoff_factor: float = 0.5, - status_forcelist: tuple = (500, 502, 504) + retries: int = 3, backoff_factor: float = 0.5, status_forcelist: tuple = (500, 502, 504) ) -> requests.Session: session = requests.Session() retry = Retry( @@ -140,10 +147,11 @@ def create_session_with_retry( status_forcelist=status_forcelist, ) adapter = HTTPAdapter(max_retries=retry) - session.mount('http://', adapter) - session.mount('https://', adapter) + session.mount("http://", adapter) + session.mount("https://", adapter) return session + def get_dola_staking() -> DolaStaking: session = create_session_with_retry() try: @@ -153,7 +161,7 @@ def get_dola_staking() -> DolaStaking: data = response.json() timestamp = data["timestamp"] if not is_timestamp_recent(timestamp): - raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp/1000)}") + raise Exception(f"Data is too old. Timestamp: {datetime.fromtimestamp(timestamp / 1000)}") return DolaStaking( dola_price_usd=data["dolaPriceUsd"], @@ -170,10 +178,22 @@ def get_dola_staking() -> DolaStaking: finally: session.close() -def get_sdola_supply() -> float: + +def get_tokens_supply() -> tuple[float, float]: client = ChainManager.get_client(Chain.MAINNET) - contract = client.eth.contract(address=SDOLA_CONTRACT, abi=SDOLA_ABI) - return contract.functions.totalSupply().call() / 1e18 + dola_contract = client.eth.contract(address=DOLA_CONTRACT, abi=ERC20_ABI) + sdola_contract = client.eth.contract(address=SDOLA_CONTRACT, abi=ERC20_ABI) + + with client.batch_requests() as batch: + batch.add(dola_contract.functions.totalSupply()) + batch.add(sdola_contract.functions.totalSupply()) + responses = client.execute_batch(batch) + if len(responses) == 2: + dola_supply, sdola_supply = responses + return dola_supply / 1e18, sdola_supply / 1e18 + else: + raise Exception(f"Expected 2 responses, got {len(responses)} from blockchain batch call") + def calculate_fed_metrics(feds: List[FedInfo], dola_price: float) -> FedMonitoringMetrics: """Calculate comprehensive Fed monitoring metrics""" @@ -202,6 +222,7 @@ def calculate_fed_metrics(feds: List[FedInfo], dola_price: float) -> FedMonitori peg_deviation=peg_deviation, ) + def monitor_firm_fed(fed: FedInfo) -> None: """Monitor FiRM Fed for risk conditions""" if fed.fed_type != FedType.FIRM: @@ -212,7 +233,7 @@ def monitor_firm_fed(fed: FedInfo) -> None: send_telegram_message( f"🚨 FiRM utilization is high: {fed.utilization_ratio:.1%}. " f"TVL: ${fed.tvl:,.0f}, Borrows: ${fed.borrows:,.0f}", - PROTOCOL_NAME + PROTOCOL_NAME, ) # Collateralization concerns @@ -222,28 +243,31 @@ def monitor_firm_fed(fed: FedInfo) -> None: send_telegram_message( f"🚨 FiRM collateralization is low: {collateralization_ratio:.1%}. " f"Supply: ${fed.supply:,.0f}, Borrows: ${fed.borrows:,.0f}", - PROTOCOL_NAME + PROTOCOL_NAME, ) else: - send_telegram_message( - "Missing supply or borrows for FiRM Fed", - PROTOCOL_NAME - ) + send_telegram_message("Missing supply or borrows for FiRM Fed", PROTOCOL_NAME) + -def monitor_overall_risk(metrics: FedMonitoringMetrics, dola_price: float) -> None: +def monitor_overall_risk(metrics: FedMonitoringMetrics, dola_price: float, dola_supply: float) -> None: """Monitor overall system risk""" if metrics.firm_utilization > 0.8: send_telegram_message( f"🚨 FiRM utilization is high: {metrics.firm_utilization:.1%}. " f"TVL: ${metrics.total_fed_supply:,.0f}, Borrows: ${metrics.total_fed_supply:,.0f}", - PROTOCOL_NAME + PROTOCOL_NAME, ) # DOLA price alerts if metrics.peg_deviation > 0.005: send_telegram_message( - f"🚨 DOLA peg deviation: {metrics.peg_deviation:.4f}. " - f"DOLA price: ${dola_price:.4f}", - PROTOCOL_NAME + f"🚨 DOLA peg deviation: {metrics.peg_deviation:.4f}. DOLA price: ${dola_price:.4f}", PROTOCOL_NAME + ) + # DOLA supply alerts + if abs(dola_supply - metrics.total_fed_supply) / dola_supply > 0.005: + send_telegram_message( + f"🚨 DOLA supply mismatch: On-chain {dola_supply:,.0f} " + f"vs API {metrics.total_fed_supply:,.0f} ({abs(dola_supply - metrics.total_fed_supply) / dola_supply:.1%} diff)", + PROTOCOL_NAME, ) @@ -256,14 +280,15 @@ def monitor_deprecated_feds(feds: List[FedInfo]) -> None: if fed.supply > 19450000: send_telegram_message( f"⚠️ Frontier Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", - PROTOCOL_NAME + PROTOCOL_NAME, ) elif fed.supply > 1000: send_telegram_message( f"⚠️ Deprecated {fed.protocol} Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", - PROTOCOL_NAME + PROTOCOL_NAME, ) + def monitor_total_fed_supply_borrows(feds: List[FedInfo]) -> None: """Monitor total Fed supply for unexpected activity""" total_fed_supply = sum(fed.supply for fed in feds) @@ -271,17 +296,18 @@ def monitor_total_fed_supply_borrows(feds: List[FedInfo]) -> None: if total_fed_borrows > total_fed_supply * 0.8: send_telegram_message( f"🚨 Total Fed borrows is greater than supply: ${total_fed_borrows:,.0f} > ${total_fed_supply:,.0f} * 0.8", - PROTOCOL_NAME + PROTOCOL_NAME, ) -def monitor_dola_staking(dola_staking: DolaStaking) -> None: + +def monitor_dola_staking(dola_staking: DolaStaking, sdola_supply_onchain: float) -> None: """Monitor DOLA staking for unexpected activity""" # DOLA Staking monitoring (existing logic) if dola_staking.s_dola_total_assets < dola_staking.s_dola_supply: send_telegram_message( f"🚨 sDOLA undercollateralized: Assets {dola_staking.s_dola_total_assets:,.0f} " f"< Supply {dola_staking.s_dola_supply:,.0f}", - PROTOCOL_NAME + PROTOCOL_NAME, ) # Exchange rate validation @@ -290,17 +316,15 @@ def monitor_dola_staking(dola_staking: DolaStaking) -> None: send_telegram_message( f"🚨 sDOLA exchange rate mismatch: Calculated {calculated_ex_rate:.4f} " f"vs API {dola_staking.s_dola_ex_rate:.4f}", - PROTOCOL_NAME + PROTOCOL_NAME, ) - # sDOLA supply verification - sdola_supply_onchain = get_sdola_supply() supply_diff_pct = abs(sdola_supply_onchain - dola_staking.s_dola_supply) / dola_staking.s_dola_supply if supply_diff_pct > 0.01: # 1% tolerance send_telegram_message( f"🚨 sDOLA supply mismatch: On-chain {sdola_supply_onchain:,.0f} " f"vs API {dola_staking.s_dola_supply:,.0f} ({supply_diff_pct:.1%} diff)", - PROTOCOL_NAME + PROTOCOL_NAME, ) @@ -317,10 +341,11 @@ def monitor_dola_staking(dola_staking: DolaStaking) -> None: monitor_firm_fed(fed) # Monitor overall risk - monitor_overall_risk(metrics, dola_staking.dola_price_usd) + dola_supply, sdola_supply = get_tokens_supply() + monitor_overall_risk(metrics, dola_staking.dola_price_usd, dola_supply) monitor_deprecated_feds(feds) monitor_total_fed_supply_borrows(feds) - monitor_dola_staking(dola_staking) + monitor_dola_staking(dola_staking, sdola_supply) print(f"✅ Monitoring completed successfully. Checked {len(feds)} Feds.") except Exception as e: From ae0f70bfd58a15399dfd10dac2deab886ec191fc Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 27 Jun 2025 13:00:43 +0200 Subject: [PATCH 5/8] feat: sdola asset verification --- inverse/inverse.py | 54 +++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/inverse/inverse.py b/inverse/inverse.py index cbb7e54..e76bd1d 100644 --- a/inverse/inverse.py +++ b/inverse/inverse.py @@ -22,6 +22,7 @@ DOLA_CONTRACT = "0x865377367054516e17014CcdED1e7d814EDC9ce4" SDOLA_CONTRACT = "0xb45ad160634c528Cc3D2926d9807104FA3157305" ERC20_ABI = load_abi("common-abi/ERC20.json") +ERC4626_ABI = load_abi("common-abi/YearnV3Vault.json") class FedType(Enum): @@ -179,20 +180,21 @@ def get_dola_staking() -> DolaStaking: session.close() -def get_tokens_supply() -> tuple[float, float]: +def get_tokens_supply() -> tuple[float, float, float]: client = ChainManager.get_client(Chain.MAINNET) dola_contract = client.eth.contract(address=DOLA_CONTRACT, abi=ERC20_ABI) - sdola_contract = client.eth.contract(address=SDOLA_CONTRACT, abi=ERC20_ABI) + sdola_contract = client.eth.contract(address=SDOLA_CONTRACT, abi=ERC4626_ABI) with client.batch_requests() as batch: batch.add(dola_contract.functions.totalSupply()) batch.add(sdola_contract.functions.totalSupply()) + batch.add(sdola_contract.functions.totalAssets()) responses = client.execute_batch(batch) - if len(responses) == 2: - dola_supply, sdola_supply = responses - return dola_supply / 1e18, sdola_supply / 1e18 + if len(responses) == 3: + dola_supply, sdola_supply, sdola_assets = responses + return dola_supply / 1e18, sdola_supply / 1e18, sdola_assets / 1e18 else: - raise Exception(f"Expected 2 responses, got {len(responses)} from blockchain batch call") + raise Exception(f"Expected 3 responses, got {len(responses)} from blockchain batch call") def calculate_fed_metrics(feds: List[FedInfo], dola_price: float) -> FedMonitoringMetrics: @@ -300,8 +302,9 @@ def monitor_total_fed_supply_borrows(feds: List[FedInfo]) -> None: ) -def monitor_dola_staking(dola_staking: DolaStaking, sdola_supply_onchain: float) -> None: +def monitor_dola_staking(dola_staking: DolaStaking, sdola_supply_onchain: float, sdola_assets_onchain: float) -> None: """Monitor DOLA staking for unexpected activity""" + max_diff = 0.001 # 0.1% # DOLA Staking monitoring (existing logic) if dola_staking.s_dola_total_assets < dola_staking.s_dola_supply: send_telegram_message( @@ -309,21 +312,38 @@ def monitor_dola_staking(dola_staking: DolaStaking, sdola_supply_onchain: float) f"< Supply {dola_staking.s_dola_supply:,.0f}", PROTOCOL_NAME, ) - - # Exchange rate validation + # Exchange rate validation off chain data calculated_ex_rate = dola_staking.s_dola_total_assets / dola_staking.s_dola_supply - if abs(calculated_ex_rate - dola_staking.s_dola_ex_rate) > 0.001: + calculated_ex_rate_diff = abs(calculated_ex_rate - dola_staking.s_dola_ex_rate) / calculated_ex_rate + if calculated_ex_rate_diff > max_diff: + send_telegram_message( + f"🚨 sDOLA exchange rate mismatch: Calculated with off chain data {calculated_ex_rate:.4f} " + f"vs API {dola_staking.s_dola_ex_rate:.4f} ({calculated_ex_rate_diff / calculated_ex_rate:.1%}%)", + PROTOCOL_NAME, + ) + # Exchange rate validation on chain data + onchain_exchange_rate = sdola_assets_onchain / sdola_supply_onchain + onchain_exchange_rate_diff = abs(onchain_exchange_rate - dola_staking.s_dola_ex_rate) / onchain_exchange_rate + if onchain_exchange_rate_diff > max_diff: send_telegram_message( - f"🚨 sDOLA exchange rate mismatch: Calculated {calculated_ex_rate:.4f} " - f"vs API {dola_staking.s_dola_ex_rate:.4f}", + f"🚨 sDOLA exchange rate mismatch: On-chain{onchain_exchange_rate:.4f} " + f"vs API {dola_staking.s_dola_ex_rate:.4f} ({onchain_exchange_rate_diff / onchain_exchange_rate:.1%}%)", PROTOCOL_NAME, ) # sDOLA supply verification - supply_diff_pct = abs(sdola_supply_onchain - dola_staking.s_dola_supply) / dola_staking.s_dola_supply - if supply_diff_pct > 0.01: # 1% tolerance + sdola_supply_diff = abs(sdola_supply_onchain - dola_staking.s_dola_supply) / sdola_supply_onchain + if sdola_supply_diff > max_diff: send_telegram_message( f"🚨 sDOLA supply mismatch: On-chain {sdola_supply_onchain:,.0f} " - f"vs API {dola_staking.s_dola_supply:,.0f} ({supply_diff_pct:.1%} diff)", + f"vs API {dola_staking.s_dola_supply:,.0f} ({sdola_supply_diff / sdola_supply_onchain:.1%}%)", + PROTOCOL_NAME, + ) + # sDOLA assets verification + sdola_assets_diff = abs(sdola_assets_onchain - dola_staking.s_dola_total_assets) / sdola_assets_onchain + if sdola_assets_diff > max_diff: + send_telegram_message( + f"🚨 sDOLA assets mismatch: On-chain {sdola_assets_onchain:,.0f} " + f"vs API {dola_staking.s_dola_total_assets:,.0f} ({sdola_assets_diff / sdola_assets_onchain:.1%}%)", PROTOCOL_NAME, ) @@ -341,11 +361,11 @@ def monitor_dola_staking(dola_staking: DolaStaking, sdola_supply_onchain: float) monitor_firm_fed(fed) # Monitor overall risk - dola_supply, sdola_supply = get_tokens_supply() + dola_supply, sdola_supply, sdola_assets = get_tokens_supply() monitor_overall_risk(metrics, dola_staking.dola_price_usd, dola_supply) monitor_deprecated_feds(feds) monitor_total_fed_supply_borrows(feds) - monitor_dola_staking(dola_staking, sdola_supply) + monitor_dola_staking(dola_staking, sdola_supply, sdola_assets) print(f"✅ Monitoring completed successfully. Checked {len(feds)} Feds.") except Exception as e: From 5fb1943336c195fbbf6830d12d584818f2da0fce Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 27 Jun 2025 13:03:59 +0200 Subject: [PATCH 6/8] docs: update inverse docs --- inverse/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/inverse/README.md b/inverse/README.md index a81f65b..9ef440a 100644 --- a/inverse/README.md +++ b/inverse/README.md @@ -8,16 +8,16 @@ The script `inverse/inverse.py` runs [hourly via GitHub Actions](../.github/work #### FiRM Monitoring -- **DOLA Supply Check**: Alerts if FiRM's circulating supply exceeds the total DOLA circulating supply by more than 9M DOLA (accounting for Frontier bad debt and Gearbox). Threshold defined [in code](inverse.py#L113). -- **TVL to Borrows Ratio**: Alerts if borrows exceed 80% of the TVL, indicating high utilization. Threshold defined [in code](inverse.py#L116). +- **DOLA Supply Check**: Alerts if FiRM's circulating supply exceeds the total DOLA circulating supply by more than 9M DOLA (accounting for Frontier bad debt and Gearbox). +- **TVL to Borrows Ratio**: Alerts if borrows exceed 80% of the TVL, indicating high utilization. #### DOLA Staking Monitoring -- **DOLA Price Stability**: Alerts if DOLA price drops below $0.998, indicating potential depegging. Threshold defined [in code](inverse.py#L123). -- **Staking Coverage**: Alerts if total staked DOLA assets are less than the sDOLA supply, indicating potential undercollateralization. Check defined [in code](inverse.py#L126). -- **Exchange Rate Validation**: Verifies that the calculated exchange rate matches the reported rate from the API. Check defined [in code](inverse.py#L129). +- **DOLA Price Stability**: Alerts if DOLA price drops below $0.998, indicating potential depegging. +- **Staking Coverage**: Alerts if total staked DOLA assets are less than the sDOLA supply, indicating potential undercollateralization. +- **Exchange Rate Validation**: Verifies that the calculated exchange rate matches the reported rate from the API. -All API responses are validated to ensure data is not older than 2 hours. Timestamp validation logic defined [in code](inverse.py#L39). +All API responses are validated to ensure data is not older than 2 hours. #### DOLA Supply Monitoring @@ -25,4 +25,4 @@ DOLA supply is checked from the contract and verified it matches the supply from #### SDOLA Supply Monitoring -Check SDOLA supply from the contract and verify it matches the supply from the API, if the difference is greater than 0.5%, telegram alert is sent. +Check SDOLA supply from the contract and verify it matches the supply from the API, if the difference is greater than 0.5%, telegram alert is sent. Exchange rate is also verified from the contract and API. From 4dfe7d85e0e34159fb1b4c23c7aeea4da8ace820 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 27 Jun 2025 14:02:16 +0200 Subject: [PATCH 7/8] feat: invernse governance --- inverse/README.md | 5 +++++ safe/main.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/inverse/README.md b/inverse/README.md index 9ef440a..2be399a 100644 --- a/inverse/README.md +++ b/inverse/README.md @@ -26,3 +26,8 @@ DOLA supply is checked from the contract and verified it matches the supply from #### SDOLA Supply Monitoring Check SDOLA supply from the contract and verify it matches the supply from the API, if the difference is greater than 0.5%, telegram alert is sent. Exchange rate is also verified from the contract and API. + +## Governance + +- Monitor [Governance timelock contract](https://etherscan.io/address/0xe082EB109fAd53eA8DB9827ce6b8ef74882734fc#readContract#F3) using Tenderly alerts. TODO: add tenderly before merging +- Monitor [chair multisig](https://etherscan.io/address/0xe082EB109fAd53eA8DB9827ce6b8ef74882734fc#readContract#F1) which can mint and burn additional liquidity. diff --git a/safe/main.py b/safe/main.py index 6df5020..c809cd9 100644 --- a/safe/main.py +++ b/safe/main.py @@ -195,6 +195,11 @@ def main(): "mainnet", "0xb7cB7131FFc18f87eEc66991BECD18f2FF70d2af", ], # LBTC boring vault big boss + [ + "INVERSE", + "mainnet", + "0x8F97cCA30Dbe80e7a8B462F1dD1a51C32accDfC8", + ], # chair multisig contract that can mint and burn additional liquidity for Gearbox # [ # "USD0", # "mainnet", From cb850b285745bfd01e0f74ddab8a178af29ac750 Mon Sep 17 00:00:00 2001 From: spalen0 Date: Fri, 27 Jun 2025 15:24:09 +0200 Subject: [PATCH 8/8] chore: additional inverse monitoring --- inverse/inverse.py | 70 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/inverse/inverse.py b/inverse/inverse.py index e76bd1d..e86e894 100644 --- a/inverse/inverse.py +++ b/inverse/inverse.py @@ -76,14 +76,14 @@ def classify_fed_type(protocol: str) -> FedType: """Classify Fed type based on protocol and name""" if protocol == "FiRM": return FedType.FIRM - elif protocol in ["Frontier", "Fuse6", "Fuse24"]: + elif protocol in ["Frontier", "Fuse", "Fuse24", "Badger", "0xb1", "Yearn", "Scream"]: return FedType.DEPRECATED - elif protocol in ["Badger", "0xb1", "Yearn", "Convex", "Scream", "Velo", "Aura", "AuraEuler", "Aero", "FraxPyusd"]: - return FedType.AMM elif protocol in ["ArbiFed", "BaseCCTP", "OptiCCTP", "Gearbox"]: return FedType.CROSS_CHAIN - else: + elif protocol in ["Velodrome", "Aerodrome", "Convex", "Aura", "AuraEuler", "Aero"]: return FedType.AMM # Default to AMM for unknown protocols + else: + return FedType.AMM def get_all_feds_overview() -> List[FedInfo]: @@ -207,6 +207,8 @@ def calculate_fed_metrics(feds: List[FedInfo], dola_price: float) -> FedMonitori if firm_feds: firm_fed = firm_feds[0] # Should only be one FiRM Fed firm_utilization = firm_fed.utilization_ratio or 0.0 + else: + raise Exception("No FiRM Fed found") # AMM Feds backing ratios amm_feds_backing = {} @@ -253,10 +255,11 @@ def monitor_firm_fed(fed: FedInfo) -> None: def monitor_overall_risk(metrics: FedMonitoringMetrics, dola_price: float, dola_supply: float) -> None: """Monitor overall system risk""" - if metrics.firm_utilization > 0.8: + print(f"Firm utilization: {metrics.firm_utilization:.1%}") + if metrics.firm_utilization > 0.80: send_telegram_message( f"🚨 FiRM utilization is high: {metrics.firm_utilization:.1%}. " - f"TVL: ${metrics.total_fed_supply:,.0f}, Borrows: ${metrics.total_fed_supply:,.0f}", + f"TVL: ${metrics.total_fed_supply:,.0f}, Borrows: ${metrics.total_fed_borrows:,.0f}", PROTOCOL_NAME, ) # DOLA price alerts @@ -284,9 +287,52 @@ def monitor_deprecated_feds(feds: List[FedInfo]) -> None: f"⚠️ Frontier Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", PROTOCOL_NAME, ) - elif fed.supply > 1000: + if fed.borrows > 6157168: + send_telegram_message( + f"⚠️ Frontier Fed has significant borrows: ${fed.borrows:,.0f}. Above the last recorded value.", + PROTOCOL_NAME, + ) + continue + if fed.supply > 1000: + send_telegram_message( + f"⚠️ Deprecated {fed.protocol} ({fed.name}) Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", + PROTOCOL_NAME, + ) + if fed.borrows > 0: + if fed.name == "Fuse6 Fed" and fed.borrows < 5432: + continue + if fed.name == "Badger Fed" and fed.borrows < 11717: + continue + if fed.name == "0xb1 Fed" and fed.borrows < 174619: + continue + send_telegram_message( + f"⚠️ Deprecated {fed.protocol} ({fed.name}) Fed has significant borrows: ${fed.borrows:,.0f}. Above the last recorded value.", + PROTOCOL_NAME, + ) + if fed.circSupply > 0: send_telegram_message( - f"⚠️ Deprecated {fed.protocol} Fed has significant supply: ${fed.supply:,.0f}. Above the last recorded value.", + f"⚠️ Deprecated {fed.protocol}({fed.name}) Fed has circSupply: ${fed.circSupply:,.0f}", + PROTOCOL_NAME, + ) + + +def monitor_amm_feds(feds: List[FedInfo]) -> None: + """Monitor AMM Feds for unexpected activity""" + amm_feds = [fed for fed in feds if fed.fed_type == FedType.AMM] + for fed in amm_feds: + if fed.circSupply > 0: + send_telegram_message( + f"⚠️ AMM {fed.protocol} Fed has circSupply: ${fed.circSupply:,.0f}", + PROTOCOL_NAME, + ) + if fed.supply > 0: + send_telegram_message( + f"⚠️ AMM {fed.protocol} Fed has supply: ${fed.supply:,.0f}", + PROTOCOL_NAME, + ) + if fed.borrows > 0: + send_telegram_message( + f"⚠️ AMM {fed.protocol} Fed has borrows: ${fed.borrows:,.0f}", PROTOCOL_NAME, ) @@ -295,9 +341,12 @@ def monitor_total_fed_supply_borrows(feds: List[FedInfo]) -> None: """Monitor total Fed supply for unexpected activity""" total_fed_supply = sum(fed.supply for fed in feds) total_fed_borrows = sum(fed.borrows for fed in feds) - if total_fed_borrows > total_fed_supply * 0.8: + print( + f"Total Fed supply: {total_fed_supply:,.0f}, Total Fed borrows: {total_fed_borrows:,.0f}, ratio: {total_fed_borrows / total_fed_supply:.1%}" + ) + if total_fed_borrows > total_fed_supply * 0.75: send_telegram_message( - f"🚨 Total Fed borrows is greater than supply: ${total_fed_borrows:,.0f} > ${total_fed_supply:,.0f} * 0.8", + f"🚨 Total Fed borrows is greater than supply: ${total_fed_borrows:,.0f} > ${total_fed_supply:,.0f} * 0.75", PROTOCOL_NAME, ) @@ -364,6 +413,7 @@ def monitor_dola_staking(dola_staking: DolaStaking, sdola_supply_onchain: float, dola_supply, sdola_supply, sdola_assets = get_tokens_supply() monitor_overall_risk(metrics, dola_staking.dola_price_usd, dola_supply) monitor_deprecated_feds(feds) + monitor_amm_feds(feds) monitor_total_fed_supply_borrows(feds) monitor_dola_staking(dola_staking, sdola_supply, sdola_assets) print(f"✅ Monitoring completed successfully. Checked {len(feds)} Feds.")