""" Wallet Risk Scoring - Composite risk across all intelligence sources. ===================================================================== Score range: 0-100 Components: Label risk (0-40) - from label categories (scam, sanctioned, etc.) Cluster risk (0-30) - from entity membership (linked scam wallets) Deployer history (0-30) - from past token deployments (rug rate, lifespan) Risk levels: 0-19 minimal 20-39 low 40-59 medium 60-79 high 80-100 critical """ import logging from typing import Any logger = logging.getLogger("wallet_memory.risk_scoring") class RiskScorer: """ Composite risk scorer combining label, cluster, and deployer signals. """ def __init__(self, storage=None, labels=None, clustering=None): self._storage = storage self._labels = labels self._clustering = clustering async def score(self, address: str, chain: str, intel: dict | None = None) -> dict[str, Any]: """ Calculate composite risk score for a wallet. Args: address: wallet address chain: chain ID intel: pre-fetched intelligence dict (avoids duplicate lookups) Returns: {"score": float 0-100, "level": str, "components": {...}, "warnings": [...]} """ address.lower().strip() intel = intel or {} components = { "label_risk": 0.0, "cluster_risk": 0.0, "deployer_risk": 0.0, } warnings = [] # ── Component 1: Label risk (0-40) ────────────────────────── label_risk = intel.get("risk_contribution", 0) if label_risk == 0: # Recalculate from labels if not provided label_risk = self._score_labels(intel) components["label_risk"] = min(40, label_risk) if any(s.get("type") in ("sanctioned",) for s in intel.get("scam_associations", [])): components["label_risk"] = 40 warnings.append("Address is OFAC sanctioned") if any(s.get("type") == "known_scam" for s in intel.get("scam_associations", [])): components["label_risk"] = max(components["label_risk"], 30) warnings.append("Address flagged as known scam") # ── Component 2: Cluster risk (0-30) ────────────────────── cluster_risk = self._score_cluster(intel) components["cluster_risk"] = min(30, cluster_risk) linked = intel.get("linked_wallets", []) if len(linked) >= 5: warnings.append(f"Linked to {len(linked)} other wallets in entity") # ── Component 3: Deployer history risk (0-30) ───────────── deployer_risk = self._score_deployer_history(intel) components["deployer_risk"] = min(30, deployer_risk) history = intel.get("deployer_history", []) scam_tokens = [t for t in history if t.get("is_scam_related")] if scam_tokens: warnings.append(f"Deployer has {len(scam_tokens)} flagged token(s)") # ── Composite ──────────────────────────────────────────── total = components["label_risk"] + components["cluster_risk"] + components["deployer_risk"] total = max(0, min(100, total)) level = self._level(total) return { "score": round(total, 1), "level": level, "components": components, "warnings": warnings, } def _score_labels(self, intel: dict) -> float: """Score label risk from intelligence dict.""" score = 0.0 labels = intel.get("labels", []) for lbl in labels: cat = lbl.get("category", "").lower() if cat == "sanctioned": score += 40 elif cat in ("scam", "phishing", "drainer"): score += 20 elif cat == "mev": score += 5 # Scam associations for _assoc in intel.get("scam_associations", []): score += 15 return min(40, score) def _score_cluster(self, intel: dict) -> float: """Score risk from entity/cluster membership.""" score = 0.0 linked = intel.get("linked_wallets", []) if not linked: return 0.0 # More linked wallets = higher entity complexity # But it only adds risk if any linked wallet has scam association for _w in linked: # If any linked wallet appears in scam_associations pass # We'd need to recursively check each linked wallet # Heuristic: entity size increases risk slightly entity_count = len(linked) if entity_count >= 10: score += 10 elif entity_count >= 5: score += 5 elif entity_count >= 3: score += 2 # Confidence of entity membership adds to risk confidence = intel.get("confidence", 0) if confidence >= 0.8: score += 10 # High-confidence entity membership elif confidence >= 0.6: score += 5 # Cross-chain presence adds risk (same entity on multiple chains) cross_chain = intel.get("cross_chain_presence", []) if len(cross_chain) >= 2: score += 10 # Could indicate a sophisticated operator elif len(cross_chain) == 1: score += 3 return min(30, score) def _score_deployer_history(self, intel: dict) -> float: """Score risk from deployer history.""" history = intel.get("deployer_history", []) if not history: return 0.0 total_deployed = len(history) scam_count = sum(1 for t in history if t.get("is_scam_related")) high_risk_count = sum(1 for t in history if t.get("scan_risk_score", 0) >= 0.7) score = 0.0 # Rug rate if total_deployed > 0: rug_rate = (scam_count + high_risk_count) / total_deployed if rug_rate >= 0.8 and total_deployed >= 3: score += 25 # Serial rugger elif rug_rate >= 0.5: score += 15 # High risk deployer elif rug_rate >= 0.3: score += 8 # Suspicious # Average lifespan avg_lifespan = 0 lifespans = [t.get("lifespan_days", 0) for t in history if t.get("lifespan_days", 0) > 0] if lifespans: avg_lifespan = sum(lifespans) / len(lifespans) if avg_lifespan > 0 and avg_lifespan < 7 and total_deployed >= 2: score += 10 # Short-lived tokens = rug pattern elif avg_lifespan < 30 and total_deployed >= 3: score += 5 # Volume of deployments if total_deployed >= 10: score += 5 # Prolific deployer return min(30, score) @staticmethod def _level(score: float) -> str: if score >= 80: return "critical" elif score >= 60: return "high" elif score >= 40: return "medium" elif score >= 20: return "low" return "minimal"