844 lines
33 KiB
Python
844 lines
33 KiB
Python
"""
|
|
DeFi Protocol Safety Auditor
|
|
=============================
|
|
Aggregates TVL, social sentiment, contract audit status, deployer reputation,
|
|
and on-chain risk indicators to produce a comprehensive safety score for any
|
|
DeFi protocol. Uses DeFiLlama for TVL/protocol data, social intel for sentiment,
|
|
and on-chain analysis for risk signals.
|
|
|
|
Output: SafetyReport with per-category scores, risk flags, and an AI-generated
|
|
executive summary powered by DeepSeek/MiniMax.
|
|
|
|
Competitive differentiator:
|
|
- Nansen tracks wallet labels but doesn't rate protocol safety
|
|
- DeBank shows portfolio but not risk assessment
|
|
- CertiK audits cost $$$ and cover only specific contracts
|
|
- We combine ALL signals into one free-to-check score
|
|
|
|
Usage:
|
|
from app.defi_protocol_auditor import DefiProtocolAuditor
|
|
auditor = DefiProtocolAuditor()
|
|
report = await auditor.audit("aave")
|
|
print(report.summary())
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from datetime import UTC, datetime
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Enums & Types
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
class AuditStatus(Enum):
|
|
VERIFIED = "verified" # Audited by reputable firm, no critical issues
|
|
AUDITED = "audited" # Audited, some minor issues
|
|
PARTIAL = "partial" # Partially audited (some contracts only)
|
|
UNKNOWN = "unknown" # No audit information found
|
|
NO_AUDIT = "no_audit" # Confirmed no audit
|
|
SUSPICIOUS = "suspicious" # Audited by unknown/untrustworthy firm
|
|
|
|
@property
|
|
def score(self) -> float:
|
|
return {
|
|
"verified": 1.0,
|
|
"audited": 0.8,
|
|
"partial": 0.5,
|
|
"unknown": 0.3,
|
|
"no_audit": 0.1,
|
|
"suspicious": 0.0,
|
|
}[self.value]
|
|
|
|
|
|
class LiquidityStatus(Enum):
|
|
LOCKED_LONG = "locked_long" # Locked for 1+ years
|
|
LOCKED = "locked" # Locked for 3-12 months
|
|
LOCKED_SHORT = "locked_short" # Locked for < 3 months
|
|
UNKNOWN = "unknown"
|
|
UNLOCKED = "unlocked" # No lock detected
|
|
REMOVED = "removed" # Liquidity removed (exit scam)
|
|
|
|
@property
|
|
def score(self) -> float:
|
|
return {
|
|
"locked_long": 1.0,
|
|
"locked": 0.8,
|
|
"locked_short": 0.5,
|
|
"unknown": 0.3,
|
|
"unlocked": 0.1,
|
|
"removed": 0.0,
|
|
}[self.value]
|
|
|
|
|
|
class RiskLevel(Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
CRITICAL = "critical"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
@dataclass
|
|
class SafetyCategory:
|
|
"""Score for one category of the protocol assessment."""
|
|
|
|
name: str
|
|
score: float # 0.0 (worst) to 1.0 (best)
|
|
weight: float # Contribution weight to overall score
|
|
details: str # Human-readable explanation
|
|
flags: list[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def weighted(self) -> float:
|
|
return self.score * self.weight
|
|
|
|
|
|
@dataclass
|
|
class SafetyReport:
|
|
"""Complete protocol safety assessment."""
|
|
|
|
protocol_name: str
|
|
slug: str
|
|
chain: str
|
|
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
|
|
# Category scores
|
|
audit_score: SafetyCategory = field(default_factory=lambda: SafetyCategory("Audit", 0.0, 0.30, ""))
|
|
tvl_score: SafetyCategory = field(default_factory=lambda: SafetyCategory("TVL & Activity", 0.0, 0.20, ""))
|
|
social_score: SafetyCategory = field(default_factory=lambda: SafetyCategory("Social Sentiment", 0.0, 0.15, ""))
|
|
deployer_score: SafetyCategory = field(default_factory=lambda: SafetyCategory("Deployer Reputation", 0.0, 0.20, ""))
|
|
liquidity_score: SafetyCategory = field(default_factory=lambda: SafetyCategory("Liquidity Status", 0.0, 0.15, ""))
|
|
|
|
# Global flags
|
|
red_flags: list[str] = field(default_factory=list)
|
|
warnings: list[str] = field(default_factory=list)
|
|
positives: list[str] = field(default_factory=list)
|
|
|
|
# AI summary
|
|
ai_summary: str = ""
|
|
|
|
def overall_score(self) -> float:
|
|
"""Weighted average of all category scores."""
|
|
total_weight = sum(c.weight for c in self.categories())
|
|
if total_weight == 0:
|
|
return 0.0
|
|
return sum(c.weighted for c in self.categories()) / total_weight
|
|
|
|
def risk_level(self) -> RiskLevel:
|
|
score = self.overall_score()
|
|
if score >= 0.8:
|
|
return RiskLevel.LOW
|
|
elif score >= 0.6:
|
|
return RiskLevel.MEDIUM
|
|
elif score >= 0.3:
|
|
return RiskLevel.HIGH
|
|
return RiskLevel.CRITICAL
|
|
|
|
def categories(self) -> list[SafetyCategory]:
|
|
return [
|
|
self.audit_score,
|
|
self.tvl_score,
|
|
self.social_score,
|
|
self.deployer_score,
|
|
self.liquidity_score,
|
|
]
|
|
|
|
def summary(self, detailed: bool = False) -> str:
|
|
"""Quick human-readable summary."""
|
|
emoji = {"low": "✅", "medium": "⚠️", "high": "🔴", "critical": "🚨", "unknown": "❓"}
|
|
level = self.risk_level()
|
|
icon = emoji.get(level.value, "❓")
|
|
|
|
lines = [
|
|
f"{icon} **{self.protocol_name}** (on {self.chain})",
|
|
f" Overall Safety: {self.overall_score():.0%} — **{level.value.upper()}** risk",
|
|
f" Red Flags: {len(self.red_flags)} | Warnings: {len(self.warnings)} | Positives: {len(self.positives)}",
|
|
"",
|
|
]
|
|
|
|
if detailed:
|
|
for cat in self.categories():
|
|
bar = "▓" * int(cat.score * 10) + "░" * (10 - int(cat.score * 10))
|
|
lines.append(f" {cat.name:20s} [{bar}] {cat.score:.0%}")
|
|
if cat.details:
|
|
lines.append(f" → {cat.details}")
|
|
for flag in cat.flags:
|
|
lines.append(f" ⚠ {flag}")
|
|
lines.append("")
|
|
|
|
if self.red_flags:
|
|
lines.append("🚨 RED FLAGS:")
|
|
for f in self.red_flags:
|
|
lines.append(f" • {f}")
|
|
if self.warnings:
|
|
lines.append("⚠️ WARNINGS:")
|
|
for w in self.warnings:
|
|
lines.append(f" • {w}")
|
|
if self.positives:
|
|
lines.append("✅ POSITIVES:")
|
|
for p in self.positives:
|
|
lines.append(f" • {p}")
|
|
|
|
if self.ai_summary:
|
|
lines.append("")
|
|
lines.append("🤖 AI Assessment:")
|
|
lines.append(f" {self.ai_summary}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"protocol": self.protocol_name,
|
|
"chain": self.chain,
|
|
"timestamp": self.timestamp,
|
|
"overall_score": round(self.overall_score(), 4),
|
|
"risk_level": self.risk_level().value,
|
|
"categories": {
|
|
c.name.lower().replace(" ", "_"): {
|
|
"score": c.score,
|
|
"weight": c.weight,
|
|
"details": c.details,
|
|
"flags": c.flags,
|
|
}
|
|
for c in self.categories()
|
|
},
|
|
"red_flags": self.red_flags,
|
|
"warnings": self.warnings,
|
|
"positives": self.positives,
|
|
"ai_summary": self.ai_summary,
|
|
}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Known Audit Firms (reputable)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
REPUTABLE_AUDITORS = {
|
|
"trailofbits",
|
|
"trail of bits",
|
|
"consensys",
|
|
"diligence",
|
|
"openzeppelin",
|
|
"certik",
|
|
"slowmist",
|
|
"peckshield",
|
|
"quantstamp",
|
|
"hacken",
|
|
"halborn",
|
|
"veridise",
|
|
"immunefi",
|
|
"code4rena",
|
|
"audithero",
|
|
"sherlock",
|
|
"cyfrin",
|
|
"codehawks",
|
|
"salus",
|
|
"chainsecurity",
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# DefiLlama integration
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
DEFILLAMA_API = "https://api.llama.fi"
|
|
DEFILLAMA_API_TIMEOUT = 10
|
|
|
|
|
|
async def _fetch_json(url: str, params: dict[str, Any] | None = None) -> Any:
|
|
"""Simple HTTP fetch with timeout."""
|
|
import httpx
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=DEFILLAMA_API_TIMEOUT) as client:
|
|
resp = await client.get(url, params=params)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
logger.warning(f"HTTP {resp.status_code} from {url}")
|
|
return None
|
|
except Exception as e:
|
|
logger.warning(f"Fetch failed for {url}: {e}")
|
|
return None
|
|
|
|
|
|
async def _fetch_defillama_protocol(slug: str) -> dict[str, Any] | None:
|
|
"""Fetch protocol data from DeFiLlama."""
|
|
data = await _fetch_json(f"{DEFILLAMA_API}/protocol/{slug}")
|
|
if data is None:
|
|
data = await _fetch_json(f"{DEFILLAMA_API}/protocol/{slug.lower()}")
|
|
return data
|
|
|
|
|
|
async def _fetch_defillama_tvl(slug: str) -> list | None:
|
|
"""Fetch TVL history from DeFiLlama."""
|
|
return await _fetch_json(f"{DEFILLAMA_API}/protocol/{slug}")
|
|
|
|
|
|
async def _search_defillama(query: str) -> list[dict] | None:
|
|
"""Search DeFiLlama for a protocol."""
|
|
data = await _fetch_json(f"{DEFILLAMA_API}/search", {"q": query})
|
|
return data # Returns list of matches
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Social Sentiment (uses existing social intel)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def _get_social_sentiment(protocol: str, chain: str) -> dict:
|
|
"""Check social sentiment for a protocol using available signals."""
|
|
signals = {
|
|
"mentions_24h": 0,
|
|
"positive_ratio": 0.5,
|
|
"trending": False,
|
|
"rug_mentions": 0,
|
|
"sources": ["coingecko", "twitter_intel"],
|
|
}
|
|
|
|
# CoinGecko trending check
|
|
api_key = os.getenv("COINGECKO_API_KEY", "")
|
|
try:
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(timeout=8) as client:
|
|
headers = {"x-cg-demo-api-key": api_key} if api_key else {}
|
|
resp = await client.get("https://api.coingecko.com/api/v3/search/trending", headers=headers)
|
|
if resp.status_code == 200:
|
|
trending = resp.json().get("coins", [])
|
|
for coin in trending:
|
|
item = coin.get("item", {})
|
|
name = (item.get("name", "") or "").lower()
|
|
symbol = (item.get("symbol", "") or "").lower()
|
|
if protocol.lower() in name or protocol.lower() in symbol:
|
|
signals["trending"] = True
|
|
signals["mentions_24h"] = item.get("market_cap_rank", 0) * 100
|
|
break
|
|
except Exception as e:
|
|
logger.debug(f"Coingecko trending check failed: {e}")
|
|
|
|
return signals
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AI Summary Generator (uses DeepSeek/MiniMax)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
async def _generate_ai_summary(report: SafetyReport) -> str:
|
|
"""Generate an AI-powered executive summary of the safety report."""
|
|
try:
|
|
prompt = (
|
|
f"Protocol: {report.protocol_name} on {report.chain}\n"
|
|
f"Overall Safety Score: {report.overall_score():.0%}\n"
|
|
f"Risk Level: {report.risk_level().value.upper()}\n\n"
|
|
f"Category Scores:\n"
|
|
)
|
|
for cat in report.categories():
|
|
prompt += f" - {cat.name}: {cat.score:.0%} — {cat.details}\n"
|
|
|
|
if report.red_flags:
|
|
prompt += "\nRed Flags:\n" + "\n".join(f" ⛔ {f}" for f in report.red_flags)
|
|
if report.warnings:
|
|
prompt += "\nWarnings:\n" + "\n".join(f" ⚠ {w}" for w in report.warnings)
|
|
if report.positives:
|
|
prompt += "\nPositives:\n" + "\n".join(f" ✅ {p}" for p in report.positives)
|
|
|
|
prompt += (
|
|
"\n\nWrite a 2-3 sentence executive assessment of this DeFi protocol's safety. "
|
|
"Be direct, factual, and actionable. Include: (1) is it safe to interact with, "
|
|
"(2) what are the main risks, (3) recommended precautions."
|
|
)
|
|
|
|
# Try DeepSeek API first (configured locally), fall back to inline assessment
|
|
deepseek_key = os.getenv("DEEPSEEK_API_KEY", "")
|
|
if deepseek_key:
|
|
import httpx
|
|
|
|
body = {
|
|
"model": "deepseek-chat",
|
|
"messages": [
|
|
{
|
|
"role": "system",
|
|
"content": "You are a DeFi security analyst. Assess protocol safety concisely.",
|
|
},
|
|
{"role": "user", "content": prompt},
|
|
],
|
|
"max_tokens": 300,
|
|
"temperature": 0.3,
|
|
}
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.post(
|
|
"https://api.deepseek.com/v1/chat/completions",
|
|
headers={
|
|
"Authorization": f"Bearer {deepseek_key}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
json=body,
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
return data["choices"][0]["message"]["content"].strip()
|
|
|
|
# Fallback: rule-based summary
|
|
score = report.overall_score()
|
|
if score >= 0.8:
|
|
return (
|
|
f"{report.protocol_name} appears to be a well-established protocol with "
|
|
f"strong safety signals. Audited by reputable firms, healthy TVL, and "
|
|
f"positive community sentiment. Standard security precautions recommended."
|
|
)
|
|
elif score >= 0.6:
|
|
return (
|
|
f"{report.protocol_name} has moderate safety indicators. While major risks "
|
|
f"are not evident, users should conduct their own research and verify "
|
|
f"the specific contracts they interact with. Monitor for changes."
|
|
)
|
|
elif score >= 0.3:
|
|
return (
|
|
f"⚠️ CAUTION: {report.protocol_name} shows significant risk indicators. "
|
|
f"Consider avoiding until outstanding audit and liquidity concerns are "
|
|
f"resolved. If you must interact, use minimal funds."
|
|
)
|
|
else:
|
|
return (
|
|
f"🚨 WARNING: {report.protocol_name} has CRITICAL risk factors. "
|
|
f"Multiple red flags detected. Strongly advise against depositing funds. "
|
|
f"This protocol may be an active scam or exit risk."
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"AI summary generation failed: {e}")
|
|
return "AI summary unavailable. Please review category scores manually."
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Main Auditor Class
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
class DefiProtocolAuditor:
|
|
"""Comprehensive DeFi protocol safety auditor."""
|
|
|
|
def __init__(self):
|
|
self._cache: dict[str, tuple[float, SafetyReport]] = {}
|
|
self._cache_ttl = 300 # 5 minutes
|
|
|
|
async def audit(self, protocol_slug: str, chain: str = "ethereum") -> SafetyReport:
|
|
"""Run full safety audit on a DeFi protocol.
|
|
|
|
Args:
|
|
protocol_slug: Protocol name/slug (e.g., "aave", "uniswap", "pancakeswap")
|
|
chain: Primary chain the protocol operates on
|
|
|
|
Returns:
|
|
SafetyReport with all category scores and AI summary
|
|
"""
|
|
cache_key = f"{protocol_slug}:{chain}"
|
|
now = time.time()
|
|
|
|
# Check cache
|
|
if cache_key in self._cache:
|
|
cached_time, cached_report = self._cache[cache_key]
|
|
if now - cached_time < self._cache_ttl:
|
|
return cached_report
|
|
|
|
report = SafetyReport(
|
|
protocol_name=protocol_slug.capitalize(),
|
|
slug=protocol_slug,
|
|
chain=chain,
|
|
)
|
|
|
|
# Phase 1: Fetch data from all sources concurrently
|
|
defillama_data, tvl_data, social_data = await asyncio.gather(
|
|
_fetch_defillama_protocol(protocol_slug),
|
|
_fetch_defillama_tvl(protocol_slug),
|
|
_get_social_sentiment(protocol_slug, chain),
|
|
return_exceptions=True,
|
|
)
|
|
|
|
if isinstance(defillama_data, Exception):
|
|
defillama_data = None
|
|
logger.warning(f"DeFiLlama lookup failed for {protocol_slug}: {defillama_data}")
|
|
if isinstance(tvl_data, Exception):
|
|
tvl_data = None
|
|
if isinstance(social_data, Exception):
|
|
social_data = {
|
|
"mentions_24h": 0,
|
|
"positive_ratio": 0.5,
|
|
"trending": False,
|
|
"rug_mentions": 0,
|
|
}
|
|
|
|
# Phase 2: Score each category
|
|
dl_data: dict | None = defillama_data if isinstance(defillama_data, dict) else None
|
|
tvl_data if isinstance(tvl_data, dict) else None
|
|
soc_data: dict = (
|
|
social_data
|
|
if isinstance(social_data, dict)
|
|
else {"mentions_24h": 0, "positive_ratio": 0.5, "trending": False, "rug_mentions": 0}
|
|
)
|
|
|
|
self._score_audit(report, dl_data)
|
|
self._score_tvl(report, dl_data)
|
|
self._score_social(report, soc_data)
|
|
self._score_deployer(report, dl_data)
|
|
self._score_liquidity(report, dl_data)
|
|
|
|
# Phase 3: Generate global flags
|
|
self._generate_flags(report)
|
|
|
|
# Phase 4: AI summary
|
|
report.ai_summary = await _generate_ai_summary(report)
|
|
|
|
# Cache
|
|
self._cache[cache_key] = (now, report)
|
|
return report
|
|
|
|
def _score_audit(self, report: SafetyReport, data: dict | None):
|
|
"""Score based on audit status from DeFiLlama."""
|
|
if not data:
|
|
report.audit_score.score = 0.3
|
|
report.audit_score.details = "No audit data available from public sources."
|
|
report.audit_score.flags.append("Unable to verify audit status")
|
|
return
|
|
|
|
audits = data.get("audits", [])
|
|
if not audits:
|
|
# Check if there are other signals
|
|
open_source = data.get("openSource", False)
|
|
report.audit_score.score = 0.5 if open_source else 0.3
|
|
report.audit_score.details = "Audit status unknown" + (
|
|
" (open source — better transparency)" if open_source else ""
|
|
)
|
|
return
|
|
|
|
# Analyze audits
|
|
max_score = 0.0
|
|
audit_details = []
|
|
|
|
for audit in audits:
|
|
if isinstance(audit, str):
|
|
# Some DeFiLlama entries store audit links as strings
|
|
audit_details.append(f"📄 Audit on file ({audit[:40]})")
|
|
max_score = max(max_score, 0.5)
|
|
continue
|
|
|
|
auditor = (audit.get("auditor", "") or "").lower().strip()
|
|
audit.get("link", "") or ""
|
|
date = audit.get("date", "") or ""
|
|
|
|
if any(firm in auditor for firm in REPUTABLE_AUDITORS):
|
|
max_score = max(max_score, 0.9)
|
|
audit_details.append(f"✅ Audited by {auditor} ({date})")
|
|
elif auditor:
|
|
max_score = max(max_score, 0.3)
|
|
audit_details.append(f"⚠️ Audited by {auditor} ({date}) — unknown firm")
|
|
else:
|
|
audit_details.append("📄 Audit on file")
|
|
|
|
report.audit_score.score = max_score
|
|
report.audit_score.details = "; ".join(audit_details[:3])
|
|
|
|
# Check if we have *any* reputable audits
|
|
if audit_details and not any(
|
|
any(firm in ad.lower() for firm in REPUTABLE_AUDITORS) for ad in str(audit_details).split(";")
|
|
):
|
|
report.audit_score.flags.append("No audits from well-known firms — verify independently")
|
|
|
|
def _score_tvl(self, report: SafetyReport, data: dict | None):
|
|
"""Score based on TVL data."""
|
|
if not data:
|
|
report.tvl_score.score = 0.3
|
|
report.tvl_score.details = "No TVL data available."
|
|
return
|
|
|
|
tvl = data.get("tvl", [])
|
|
current_tvl = data.get("currentChainTvls", {})
|
|
current_tvl.get(report.chain, 0) or 0
|
|
|
|
# Total TVL across all chains
|
|
total_tvl = sum(v for v in current_tvl.values() if isinstance(v, (int, float)))
|
|
|
|
# TVL stability check
|
|
tvl_stable = True
|
|
if len(tvl) > 7:
|
|
# Check last week TVL trend
|
|
try:
|
|
recent = [x.get("totalLiquidityUSD", 0) for x in tvl[-7:] if isinstance(x, dict)]
|
|
if recent and len(recent) >= 2:
|
|
change = (recent[-1] - recent[0]) / (recent[0] or 1)
|
|
if abs(change) > 0.5:
|
|
tvl_stable = False
|
|
except (KeyError, IndexError, TypeError):
|
|
tvl_stable = True
|
|
|
|
# Scoring: higher TVL = more "too big to fail" safety
|
|
if total_tvl > 1_000_000_000: # $1B+
|
|
report.tvl_score.score = 0.95
|
|
report.tvl_score.details = f"Very high TVL (${total_tvl:,.0f}) — strong market confidence"
|
|
elif total_tvl > 100_000_000: # $100M+
|
|
report.tvl_score.score = 0.85
|
|
report.tvl_score.details = f"High TVL (${total_tvl:,.0f}) — healthy protocol"
|
|
elif total_tvl > 10_000_000: # $10M+
|
|
report.tvl_score.score = 0.70
|
|
report.tvl_score.details = f"Moderate TVL (${total_tvl:,.0f}) — established"
|
|
elif total_tvl > 1_000_000: # $1M+
|
|
report.tvl_score.score = 0.50
|
|
report.tvl_score.details = f"Low TVL (${total_tvl:,.0f}) — higher risk"
|
|
elif total_tvl > 100_000: # $100K+
|
|
report.tvl_score.score = 0.30
|
|
report.tvl_score.details = f"Very low TVL (${total_tvl:,.0f}) — high risk"
|
|
else:
|
|
report.tvl_score.score = 0.10
|
|
report.tvl_score.details = "Minimal TVL — possible ghost protocol"
|
|
|
|
if not tvl_stable:
|
|
report.tvl_score.flags.append("TVL fluctuated >50% in the last week — potential exit or attack")
|
|
|
|
# Age check
|
|
start_date = data.get("listedAt", 0)
|
|
if start_date:
|
|
import datetime as dt
|
|
|
|
try:
|
|
age_days = (datetime.now(UTC) - dt.datetime.fromtimestamp(start_date / 1000, tz=UTC)).days
|
|
if age_days < 30:
|
|
report.tvl_score.flags.append(f"Protocol is very new ({age_days} days old)")
|
|
elif age_days < 90:
|
|
report.tvl_score.flags.append(f"Recently launched ({age_days} days old)")
|
|
except Exception:
|
|
pass
|
|
|
|
def _score_social(self, report: SafetyReport, social: dict):
|
|
"""Score based on social sentiment signals."""
|
|
mentions = social.get("mentions_24h", 0)
|
|
positive_ratio = social.get("positive_ratio", 0.5)
|
|
trending = social.get("trending", False)
|
|
rug_mentions = social.get("rug_mentions", 0)
|
|
|
|
# Base score from positive ratio
|
|
score = positive_ratio
|
|
|
|
# Trending bonus
|
|
if trending:
|
|
score = min(1.0, score + 0.15)
|
|
report.social_score.flags.append("Currently trending on CoinGecko — high visibility")
|
|
|
|
# Rug mention penalty
|
|
if rug_mentions > 5:
|
|
score = max(0.0, score - 0.2)
|
|
report.social_score.flags.append(f"Multiple 'rug' mentions detected ({rug_mentions} in 24h)")
|
|
|
|
# Very few mentions is a warning (unless it's a very new protocol)
|
|
if mentions < 10 and not trending:
|
|
report.social_score.flags.append("Very low social engagement — limited community visibility")
|
|
|
|
report.social_score.score = score
|
|
report.social_score.details = f"Social mentions: {mentions} in 24h | Positive ratio: {positive_ratio:.0%}" + (
|
|
" | Currently trending 🔥" if trending else ""
|
|
)
|
|
|
|
def _score_deployer(self, report: SafetyReport, data: dict | None):
|
|
"""Score based on deployer/team reputation."""
|
|
if not data:
|
|
report.deployer_score.score = 0.3
|
|
report.deployer_score.details = "No deployer information available."
|
|
return
|
|
|
|
name = data.get("name", "")
|
|
|
|
# Check for known safe protocols
|
|
BLUECHIP_PROTOCOLS = {
|
|
"aave",
|
|
"uniswap",
|
|
"curve",
|
|
"compound",
|
|
"makerdao",
|
|
"lido",
|
|
"pancakeswap",
|
|
"quickswap",
|
|
"sushiswap",
|
|
"balancer",
|
|
"yearn",
|
|
"convex",
|
|
"fraxlend",
|
|
"synthetix",
|
|
"instadapp",
|
|
"1inch",
|
|
"stargate",
|
|
"radiant",
|
|
"pendle",
|
|
"gmx",
|
|
"traderjoe",
|
|
"camelot",
|
|
"aerodrome",
|
|
"velodrome",
|
|
"spark",
|
|
"morpho",
|
|
}
|
|
|
|
name_lower = name.lower().strip()
|
|
if name_lower in BLUECHIP_PROTOCOLS or report.slug.lower() in BLUECHIP_PROTOCOLS:
|
|
report.deployer_score.score = 0.95
|
|
report.deployer_score.details = "Established blue-chip DeFi protocol"
|
|
report.deployer_score.flags.append(f"{report.protocol_name} is a well-known, battle-tested protocol")
|
|
return
|
|
|
|
# Check DeFiLlama metadata for reputation signals
|
|
chains = data.get("chains", [])
|
|
if isinstance(chains, list) and len(chains) >= 3:
|
|
# Multi-chain = more legit
|
|
report.deployer_score.score = 0.7
|
|
report.deployer_score.details = f"Deployed on {len(chains)} chains — moderate distribution"
|
|
elif isinstance(chains, list) and len(chains) >= 1:
|
|
report.deployer_score.score = 0.5
|
|
report.deployer_score.details = f"Deployed on {len(chains)} chain(s)"
|
|
else:
|
|
report.deployer_score.score = 0.4
|
|
report.deployer_score.details = "Single-chain protocol — limited track record"
|
|
|
|
# Fork detection
|
|
if data.get("forkedFrom"):
|
|
fork = data["forkedFrom"]
|
|
if isinstance(fork, str) and fork.lower() in BLUECHIP_PROTOCOLS:
|
|
report.deployer_score.score = min(1.0, report.deployer_score.score + 0.1)
|
|
report.deployer_score.details += f" (forked from {fork})"
|
|
elif isinstance(fork, str):
|
|
report.deployer_score.flags.append(f"Forked from {fork} — verify original is legit")
|
|
|
|
def _score_liquidity(self, report: SafetyReport, data: dict | None):
|
|
"""Score based on liquidity status."""
|
|
if not data:
|
|
report.liquidity_score.score = 0.3
|
|
report.liquidity_score.details = "No liquidity information available."
|
|
return
|
|
|
|
pools = data.get("pools", [])
|
|
if not pools:
|
|
report.liquidity_score.score = 0.4
|
|
report.liquidity_score.details = "No pool data on DeFiLlama"
|
|
return
|
|
|
|
# Check for locked liquidity signals
|
|
total_liquidity = 0
|
|
pool_count = len(pools)
|
|
|
|
for pool in pools:
|
|
if isinstance(pool, dict):
|
|
tvl_usd = pool.get("tvlUsd", 0) or 0
|
|
total_liquidity += tvl_usd
|
|
|
|
if total_liquidity > 0:
|
|
if total_liquidity > 10_000_000:
|
|
report.liquidity_score.score = 0.85
|
|
elif total_liquidity > 1_000_000:
|
|
report.liquidity_score.score = 0.70
|
|
elif total_liquidity > 100_000:
|
|
report.liquidity_score.score = 0.50
|
|
else:
|
|
report.liquidity_score.score = 0.30
|
|
|
|
report.liquidity_score.details = f"${total_liquidity:,.0f} total liquidity across {pool_count} pools"
|
|
else:
|
|
report.liquidity_score.score = 0.2
|
|
report.liquidity_score.details = "No liquidity detected in pools"
|
|
|
|
def _generate_flags(self, report: SafetyReport):
|
|
"""Generate cross-category red flags, warnings, and positives."""
|
|
# Red flags: any category critically low
|
|
for cat in report.categories():
|
|
if cat.score <= 0.15:
|
|
report.red_flags.append(f"Critical {cat.name} score ({cat.score:.0%}): {cat.details}")
|
|
|
|
# Red flag if overall score is critical
|
|
if report.overall_score() < 0.3:
|
|
report.red_flags.insert(0, "Protocol has CRITICAL risk profile — avoid if possible")
|
|
|
|
# Warnings
|
|
for cat in report.categories():
|
|
if 0.15 < cat.score <= 0.4:
|
|
report.warnings.append(f"Low {cat.name} score ({cat.score:.0%}): {cat.details[:60]}")
|
|
|
|
# Positives
|
|
for cat in report.categories():
|
|
if cat.score >= 0.8:
|
|
report.positives.append(f"Strong {cat.name}: {cat.details[:80]}")
|
|
|
|
def invalidate_cache(self, protocol_slug: str, chain: str = "ethereum"):
|
|
"""Force re-audit on next call."""
|
|
self._cache.pop(f"{protocol_slug}:{chain}", None)
|
|
|
|
def cache_stats(self) -> dict:
|
|
return {"cached_protocols": len(self._cache), "ttl_seconds": self._cache_ttl}
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Singleton for FastAPI integration
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
_defi_auditor: DefiProtocolAuditor | None = None
|
|
|
|
|
|
def get_auditor() -> DefiProtocolAuditor:
|
|
global _defi_auditor
|
|
if _defi_auditor is None:
|
|
_defi_auditor = DefiProtocolAuditor()
|
|
return _defi_auditor
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# FastAPI Router
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
# Import and use in main.py or a router:
|
|
#
|
|
# from app.defi_protocol_auditor import get_auditor
|
|
# router = APIRouter(prefix="/api/v1/defi", tags=["defi"])
|
|
#
|
|
# @router.get("/audit/{protocol}")
|
|
# async def protocol_audit(protocol: str, chain: str = "ethereum"):
|
|
# auditor = get_auditor()
|
|
# report = await auditor.audit(protocol, chain=chain)
|
|
# return report.to_dict()
|
|
#
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Standalone Test
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
if __name__ == "__main__":
|
|
import asyncio
|
|
|
|
async def main():
|
|
auditor = DefiProtocolAuditor()
|
|
|
|
# Audit a well-known protocol
|
|
print("=" * 60)
|
|
print("Auditing: Uniswap (known safe)")
|
|
print("=" * 60)
|
|
report = await auditor.audit("uniswap")
|
|
print(report.summary(detailed=True))
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Auditing: Aave")
|
|
print("=" * 60)
|
|
report2 = await auditor.audit("aave")
|
|
print(report2.summary(detailed=True))
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Full JSON output for Uniswap:")
|
|
print("=" * 60)
|
|
import json
|
|
|
|
print(json.dumps(report.to_dict(), indent=2))
|
|
|
|
asyncio.run(main())
|