rmi-backend/app/routers/tvl_verifier.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

140 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""#16 - DeFiLlama Protocol Mismatch Detector. Compares DeFiLlama TVL against
actual on-chain balances. Flags inflated TVL or impending rug."""
import asyncio
import os
from typing import Any
import httpx
from fastapi import APIRouter, HTTPException, Query
router = APIRouter(prefix="/api/v1/tvl-verify", tags=["tvl-verifier"])
DEFILLAMA_API = "https://api.llama.fi"
DATABUS = os.environ.get("DATABUS_URL", "http://localhost:8000/api/v1/databus")
async def _get_defillama_tvl(protocol_slug: str) -> dict[str, Any]:
"""Get current TVL from DeFiLlama."""
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{DEFILLAMA_API}/protocol/{protocol_slug}")
if resp.status_code == 200:
data = resp.json()
return {
"name": data.get("name", protocol_slug),
"slug": protocol_slug,
"tvl_usd": data.get("tvl", 0),
"chain_tvls": data.get("chainTvls", {}),
"tvl_change_24h": data.get("change_1d"),
}
except Exception:
pass
return {
"name": protocol_slug,
"slug": protocol_slug,
"tvl_usd": 0,
"chain_tvls": {},
"error": "DeFiLlama unavailable",
}
async def _get_onchain_tvl(protocol_name: str, chains: list[str]) -> dict[str, float]:
"""Get actual on-chain TVL from DataBus for given chains."""
chain_tvls: dict[str, float] = {}
try:
async with httpx.AsyncClient(timeout=15) as client:
tasks = []
for c in chains[:5]:
tasks.append(client.get(f"{DATABUS}/{c}/tvl/{protocol_name.lower()}"))
responses = await asyncio.gather(*tasks, return_exceptions=True)
for i, resp in enumerate(responses):
if not isinstance(resp, Exception) and resp.status_code == 200:
data = resp.json().get("data", {})
chain_tvls[chains[i]] = data.get("tvl_usd", 0) or 0
except Exception:
pass
return chain_tvls
def _compute_discrepancy(defl_tvl: float, onchain_tvl: float) -> dict:
"""Compute TVL discrepancy with risk assessment."""
if defl_tvl <= 0 or onchain_tvl <= 0:
return {"discrepancy_pct": 0, "risk": "unknown", "explanation": "Insufficient data"}
diff_pct = ((defl_tvl - onchain_tvl) / onchain_tvl) * 100
if diff_pct > 30:
risk = "CRITICAL"
explanation = f"DeFiLlama reports {diff_pct:.0f}% higher TVL than on-chain. Possible inflated TVL."
elif diff_pct > 15:
risk = "HIGH"
explanation = f"Significant discrepancy ({diff_pct:.0f}%). Verify manually."
elif diff_pct < -30:
risk = "HIGH"
explanation = f"On-chain TVL exceeds reported TVL by {abs(diff_pct):.0f}%. Possible unreported risk."
elif diff_pct < -10:
risk = "MEDIUM"
explanation = f"On-chain TVL higher than reported ({abs(diff_pct):.0f}%)."
else:
risk = "LOW"
explanation = f"TVL aligned within {abs(diff_pct):.0f}%."
return {"discrepancy_pct": round(diff_pct, 1), "risk": risk, "explanation": explanation}
@router.get("/protocol/{slug}")
async def verify_protocol_tvl(slug: str):
"""Verify a DeFiLlama protocol's TVL against on-chain data."""
defl = await _get_defillama_tvl(slug)
# Get chain list from DeFiLlama
chain_tvls = defl.get("chain_tvls", {})
chains = list(chain_tvls.keys()) if isinstance(chain_tvls, dict) else []
# Fetch on-chain data for top chains
onchain = await _get_onchain_tvl(slug, chains[:5] if chains else ["ethereum"])
total_onchain = sum(onchain.values())
discrepancy = _compute_discrepancy(defl.get("tvl_usd", 0) or 0, total_onchain)
return {
"protocol": defl.get("name", slug),
"slug": slug,
"defillama_tvl": defl.get("tvl_usd", 0) or 0,
"onchain_tvl": round(total_onchain, 2),
"chains_verified": len(onchain),
"discrepancy": discrepancy,
"chain_breakdown": {
"defillama": {c: chain_tvls.get(c, 0) for c in chains[:5]},
"onchain": onchain,
},
}
@router.get("/top-mismatches")
async def find_top_mismatches(limit: int = Query(10, le=20)):
"""Scan top DeFiLlama protocols for TVL mismatches."""
# Fetch top protocols from DeFiLlama
top_protocols: list[str] = []
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{DEFILLAMA_API}/protocols")
if resp.status_code == 200:
protocols = resp.json()
top_protocols = [p["slug"] for p in protocols[: limit * 2] if p.get("slug")]
except Exception:
raise HTTPException(502, "DeFiLlama unavailable") from None
mismatches: list[dict] = []
for slug in top_protocols[: limit * 2]:
result = await verify_protocol_tvl(slug)
disc = result.get("discrepancy", {})
if disc.get("risk") in ("CRITICAL", "HIGH"):
mismatches.append(result)
if len(mismatches) >= limit:
break
mismatches.sort(key=lambda m: abs(m["discrepancy"].get("discrepancy_pct", 0)), reverse=True)
return {"mismatches": mismatches, "total_scanned": len(top_protocols[: limit * 2])}