rugmuncher-backend / app /wallet_memory /risk_scoring.py
Crypto Rug Muncher
clean slate: removing all old data, fresh build
6993919
Raw
History Blame Contribute Delete
7.32 kB
"""
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"