| """ |
| 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 = [] |
|
|
| |
| label_risk = intel.get("risk_contribution", 0) |
| if label_risk == 0: |
| |
| 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") |
|
|
| |
| 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") |
|
|
| |
| 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)") |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| |
| for _w in linked: |
| |
| pass |
|
|
| |
| entity_count = len(linked) |
| if entity_count >= 10: |
| score += 10 |
| elif entity_count >= 5: |
| score += 5 |
| elif entity_count >= 3: |
| score += 2 |
|
|
| |
| confidence = intel.get("confidence", 0) |
| if confidence >= 0.8: |
| score += 10 |
| elif confidence >= 0.6: |
| score += 5 |
|
|
| |
| cross_chain = intel.get("cross_chain_presence", []) |
| if len(cross_chain) >= 2: |
| score += 10 |
| |
| 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 |
|
|
| |
| if total_deployed > 0: |
| rug_rate = (scam_count + high_risk_count) / total_deployed |
| if rug_rate >= 0.8 and total_deployed >= 3: |
| score += 25 |
| elif rug_rate >= 0.5: |
| score += 15 |
| elif rug_rate >= 0.3: |
| score += 8 |
|
|
| |
| 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 |
| elif avg_lifespan < 30 and total_deployed >= 3: |
| score += 5 |
|
|
| |
| if total_deployed >= 10: |
| score += 5 |
|
|
| 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" |
|
|