| """ |
| RugCharts Token Security Matrix |
| ================================ |
| 37+ security checks across 3 tiers: Quick Scan, Deep Scan, ML Scan. |
| |
| Tier 1 (Quick Scan β <500ms): GoPlus, honeypot, taxes, basic contract checks |
| Tier 2 (Deep Scan β 2-10s): Bytecode analysis, liquidity analysis, holder distribution |
| Tier 3 (ML Scan β async): XGBoost risk classifier, bytecode anomaly, symbol executor |
| |
| Wired into DataBus as 'token_security' chain. |
| Produces the Authentic Score that feeds directly into the scanner. |
| |
| Scoring bands: |
| 0-20: SAFE (green) |
| 21-40: LOW RISK (light green) |
| 41-60: MEDIUM RISK (yellow) |
| 61-80: HIGH RISK (orange) |
| 81-100: DANGER (red) β auto-fail |
| """ |
|
|
| import json |
| import logging |
| import os |
| import time |
| from collections import defaultdict |
| from datetime import UTC, datetime |
| from typing import Any |
|
|
| import httpx |
| import redis |
|
|
| logger = logging.getLogger("token_security") |
|
|
| REDIS_HOST = os.getenv("REDIS_HOST", "rmi-redis") |
| REDIS_PORT = int(os.getenv("REDIS_PORT", "6379")) |
| REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "") |
|
|
| CACHE_TTL = 300 |
|
|
| |
|
|
|
|
| class SecurityCheck: |
| """A single security check with weight, category, and scoring.""" |
|
|
| __slots__ = ("auto_fail", "category", "description", "id", "name", "tier", "weight") |
|
|
| def __init__( |
| self, |
| id: str, |
| name: str, |
| category: str, |
| weight: float = 1.0, |
| tier: int = 1, |
| description: str = "", |
| auto_fail: bool = False, |
| ): |
| self.id = id |
| self.name = name |
| self.category = category |
| self.weight = weight |
| self.tier = tier |
| self.description = description |
| self.auto_fail = auto_fail |
|
|
|
|
| |
|
|
| SECURITY_CHECKS = [ |
| |
| SecurityCheck( |
| "CR01", |
| "Contract Verified", |
| "contract_risks", |
| 1.5, |
| 1, |
| "Contract source code is verified on explorer", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "CR02", |
| "Proxy Contract", |
| "contract_risks", |
| 2.0, |
| 1, |
| "Upgradeable proxy β owner can change logic at any time", |
| ), |
| SecurityCheck( |
| "CR03", |
| "Mint Function", |
| "contract_risks", |
| 3.0, |
| 1, |
| "Token has unrestricted mint() β infinite supply possible", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "CR04", |
| "Owner Privileges", |
| "contract_risks", |
| 2.5, |
| 1, |
| "Owner can pause trading, blacklist addresses, or adjust fees", |
| ), |
| SecurityCheck( |
| "CR05", |
| "Self-Destruct", |
| "contract_risks", |
| 3.0, |
| 1, |
| "Contract has SELFDESTRUCT opcode", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "CR06", |
| "External Call Risk", |
| "contract_risks", |
| 1.5, |
| 2, |
| "Unchecked external calls that could enable reentrancy", |
| ), |
| SecurityCheck( |
| "CR07", |
| "Delegatecall Risk", |
| "contract_risks", |
| 2.0, |
| 2, |
| "Use of delegatecall to untrusted contracts", |
| ), |
| |
| SecurityCheck( |
| "HP01", |
| "Honeypot β GoPlus", |
| "honeypot", |
| 5.0, |
| 1, |
| "GoPlus API reports honeypot: buy OK, sell blocked", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "HP02", |
| "Honeypot β Honeypot.is", |
| "honeypot", |
| 5.0, |
| 2, |
| "Honeypot.is simulation confirms trapped tokens", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "HP03", |
| "Sell Tax Anomaly", |
| "honeypot", |
| 3.0, |
| 1, |
| "Buy tax normal but sell tax >50% β likely honeypot", |
| ), |
| SecurityCheck( |
| "HP04", |
| "Transfer Pausability", |
| "honeypot", |
| 2.5, |
| 1, |
| "Owner can pause token transfers entirely", |
| ), |
| SecurityCheck( |
| "HP05", |
| "Max Transaction Limit", |
| "honeypot", |
| 1.5, |
| 1, |
| "Extremely small max tx amount blocks legitimate sales", |
| ), |
| |
| SecurityCheck( |
| "LR01", |
| "Liquidity Locked", |
| "liquidity", |
| 2.0, |
| 1, |
| "LP tokens are time-locked or burned", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "LR02", |
| "Liquidity Amount", |
| "liquidity", |
| 1.5, |
| 1, |
| "Pool liquidity < $1,000 β extreme slippage risk", |
| ), |
| SecurityCheck( |
| "LR03", |
| "LP Holder Concentration", |
| "liquidity", |
| 2.0, |
| 1, |
| "Single wallet holds >50% of LP tokens", |
| ), |
| SecurityCheck( |
| "LR04", |
| "Liquidity/Supply Ratio", |
| "liquidity", |
| 1.0, |
| 1, |
| "Low liquidity relative to total supply", |
| ), |
| SecurityCheck( |
| "LR05", |
| "Creator LP Removal", |
| "liquidity", |
| 3.0, |
| 2, |
| "Creator wallet removed significant liquidity", |
| ), |
| |
| SecurityCheck("HD01", "Top 10 Concentration", "holders", 2.0, 1, "Top 10 holders control >50% of supply"), |
| SecurityCheck("HD02", "Creator Holdings", "holders", 2.5, 1, "Creator/deployer wallet holds >5% supply"), |
| SecurityCheck("HD03", "Holder Count", "holders", 1.0, 1, "Very few holders suggests no organic adoption"), |
| SecurityCheck("HD04", "Whale Dominance", "holders", 1.5, 2, "Wallets >1% supply own >80% collectively"), |
| SecurityCheck( |
| "HD05", |
| "Fresh Wallet %", |
| "holders", |
| 1.5, |
| 2, |
| "High % of brand-new wallets (<7 days old) = bot risk", |
| ), |
| |
| SecurityCheck( |
| "TA01", |
| "Wash Trading %", |
| "trading", |
| 3.0, |
| 1, |
| "Estimated fake volume percentage from authenticity scorer", |
| ), |
| SecurityCheck( |
| "TA02", |
| "Volume/Liquidity Ratio", |
| "trading", |
| 1.5, |
| 1, |
| "Suspiciously high volume relative to thin liquidity", |
| ), |
| SecurityCheck( |
| "TA03", |
| "Buy/Sell Imbalance", |
| "trading", |
| 1.0, |
| 1, |
| "Extreme buy or sell ratio suggests manipulation", |
| ), |
| SecurityCheck("TA04", "Trade Count", "trading", 0.5, 1, "Zero or near-zero trading activity"), |
| SecurityCheck("TA05", "Price Impact", "trading", 2.0, 2, "Single trade moves price >30%"), |
| |
| SecurityCheck( |
| "DT01", |
| "Deployer History", |
| "deployer", |
| 3.0, |
| 2, |
| "Deployer wallet previously launched scam tokens", |
| ), |
| SecurityCheck("DT02", "Deployer Funding", "deployer", 2.0, 2, "Deployer funded from Tornado Cash or mixer"), |
| SecurityCheck( |
| "DT03", |
| "Multi-Token Deployer", |
| "deployer", |
| 2.5, |
| 1, |
| "Deployer launched 10+ tokens β factory pattern", |
| ), |
| SecurityCheck( |
| "DT04", |
| "Team Wallet Tracing", |
| "deployer", |
| 1.5, |
| 2, |
| "Connected wallets show suspicious activity", |
| ), |
| SecurityCheck( |
| "DT05", |
| "Social Presence", |
| "deployer", |
| 0.5, |
| 1, |
| "No website, Twitter, or Telegram β likely ghost token", |
| ), |
| |
| SecurityCheck( |
| "TE01", |
| "Supply Concentration", |
| "tokenomics", |
| 2.0, |
| 1, |
| "Creator/team controls >20% of total supply", |
| ), |
| SecurityCheck("TE02", "Tax Anomaly", "tokenomics", 2.5, 1, "Buy/sell tax >10% β predatory economics"), |
| SecurityCheck( |
| "TE03", |
| "Transfer Fee", |
| "tokenomics", |
| 1.5, |
| 1, |
| "Hidden transfer fees beyond standard DEX swap tax", |
| ), |
| SecurityCheck( |
| "TE04", |
| "Blacklist Function", |
| "tokenomics", |
| 2.0, |
| 1, |
| "Owner can blacklist specific addresses from trading", |
| ), |
| SecurityCheck( |
| "TE05", |
| "Max Wallet Cap", |
| "tokenomics", |
| 1.0, |
| 2, |
| "Artificially low max wallet cap prevents accumulation", |
| ), |
| |
| SecurityCheck( |
| "RP01", |
| "Rug Pull β Known Pattern", |
| "rug_pull", |
| 5.0, |
| 2, |
| "Token matches known rug pull deployment pattern", |
| auto_fail=True, |
| ), |
| SecurityCheck( |
| "RP02", |
| "Liquidity Removal", |
| "rug_pull", |
| 5.0, |
| 1, |
| "LP was removed or significantly drained", |
| auto_fail=True, |
| ), |
| SecurityCheck("RP03", "Price Crash", "rug_pull", 2.0, 2, "Price dropped >90% within 24h β possible rug"), |
| SecurityCheck( |
| "RP04", |
| "Duplicate Token", |
| "rug_pull", |
| 1.5, |
| 2, |
| "Same name/symbol as another token β impersonation", |
| ), |
| SecurityCheck( |
| "RP05", |
| "Age Risk", |
| "rug_pull", |
| 1.0, |
| 1, |
| "Token deployed within last hour β highest risk period", |
| ), |
| ] |
|
|
|
|
| def get_checks_by_tier(tier: int) -> list[SecurityCheck]: |
| return [c for c in SECURITY_CHECKS if c.tier <= tier] |
|
|
|
|
| def get_check_matrix() -> dict: |
| """Full security check matrix with descriptions.""" |
| categories = defaultdict(list) |
| for check in SECURITY_CHECKS: |
| categories[check.category].append( |
| { |
| "id": check.id, |
| "name": check.name, |
| "weight": check.weight, |
| "tier": check.tier, |
| "auto_fail": check.auto_fail, |
| "description": check.description, |
| } |
| ) |
| return { |
| "total_checks": len(SECURITY_CHECKS), |
| "categories": dict(categories), |
| "tiers": { |
| 1: "Quick Scan (<500ms) β GoPlus, honeypot, basic contract", |
| 2: "Deep Scan (2-10s) β Bytecode, liquidity, deployer tracing", |
| 3: "ML Scan (async) β XGBoost, bytecode anomaly, symbolic executor", |
| }, |
| } |
|
|
|
|
| |
|
|
|
|
| class TokenSecurityScorer: |
| """Weighs and aggregates security check results into a 0-100 risk score.""" |
|
|
| SCORE_BANDS = [ |
| (0, 20, "SAFE", "#00FF88"), |
| (21, 40, "LOW RISK", "#88FF00"), |
| (41, 60, "MEDIUM RISK", "#FFD700"), |
| (61, 80, "HIGH RISK", "#FF8800"), |
| (81, 100, "DANGER", "#FF0044"), |
| ] |
|
|
| @staticmethod |
| def band_for_score(score: float) -> tuple[str, str]: |
| for lo, hi, band, color in TokenSecurityScorer.SCORE_BANDS: |
| if lo <= score <= hi: |
| return band, color |
| return "DANGER", "#FF0044" |
|
|
| @staticmethod |
| def compute_score(check_results: dict[str, Any]) -> tuple[float, str, str]: |
| """Compute weighted risk score from check results. |
| |
| Each check result: {"status": "pass"|"fail"|"warning"|"unknown", "details": str} |
| |
| Returns (score 0-100, band, color) |
| """ |
| total_weight = 0.0 |
| weighted_score = 0.0 |
| auto_fail_triggered = False |
| check_details = [] |
|
|
| |
| checks_by_id = {c.id: c for c in SECURITY_CHECKS} |
|
|
| for check_id, result in check_results.items(): |
| check = checks_by_id.get(check_id) |
| if not check: |
| continue |
|
|
| status = result.get("status", "unknown") |
| total_weight += check.weight |
|
|
| if status == "fail": |
| weighted_score += check.weight * 100.0 |
| check_details.append(f"FAIL {check.id} {check.name}: {result.get('details', '')}") |
| if check.auto_fail: |
| auto_fail_triggered = True |
| elif status == "warning": |
| weighted_score += check.weight * 65.0 |
| check_details.append(f"WARN {check.id} {check.name}: {result.get('details', '')}") |
| elif status == "unknown": |
| weighted_score += check.weight * 30.0 |
| |
|
|
| if total_weight == 0: |
| return 50.0, "MEDIUM RISK", "#FFD700" |
|
|
| score = weighted_score / total_weight |
|
|
| if auto_fail_triggered: |
| score = max(score, 85.0) |
|
|
| band, color = TokenSecurityScorer.band_for_score(score) |
| return round(score, 1), band, color |
|
|
|
|
| |
|
|
|
|
| async def run_quick_scan(address: str, chain: str = "ethereum", **kw) -> dict[str, Any]: |
| """Tier 1 quick scan β GoPlus + basic checks. <1s target.""" |
| results = {} |
| api_key = kw.get("api_key", "") or kw.get("goplus_key", "") or os.getenv("GOPLUS_API_KEY", "") |
|
|
| try: |
| |
| chain_map = { |
| "ethereum": "1", |
| "eth": "1", |
| "bsc": "56", |
| "polygon": "137", |
| "arbitrum": "42161", |
| "optimism": "10", |
| "base": "8453", |
| "avalanche": "43114", |
| "fantom": "250", |
| "gnosis": "100", |
| } |
| goplus_chain = chain_map.get(chain.lower(), chain) |
|
|
| |
| async with httpx.AsyncClient(timeout=5) as c: |
| |
| goplus_url = f"https://api.gopluslabs.io/api/v1/token_security/{goplus_chain}" |
| addr_lower = address.lower() |
| r = await c.get(goplus_url, params={"contract_addresses": addr_lower}) |
| if r.status_code == 200: |
| goplus_data = r.json().get("result", {}).get(address.lower(), {}) |
|
|
| |
| is_honeypot = goplus_data.get("is_honeypot", "0") |
| results["HP01"] = { |
| "status": "fail" if is_honeypot in ("1", "1.0") else "pass", |
| "details": f"GoPlus honeypot={'YES' if is_honeypot in ('1', '1.0') else 'no'}", |
| } |
|
|
| |
| is_open_source = goplus_data.get("is_open_source", "0") |
| results["CR01"] = { |
| "status": "pass" if is_open_source in ("1", "1.0") else "fail", |
| "details": f"Verified={'YES' if is_open_source in ('1', '1.0') else 'NO'}", |
| } |
|
|
| is_proxy = goplus_data.get("is_proxy", "0") |
| results["CR02"] = { |
| "status": "warning" if is_proxy in ("1", "1.0") else "pass", |
| "details": f"Proxy={'YES' if is_proxy in ('1', '1.0') else 'no'}", |
| } |
|
|
| is_mintable = goplus_data.get("is_mintable", "0") |
| results["CR03"] = { |
| "status": "fail" if is_mintable in ("1", "1.0") else "pass", |
| "details": f"Mintable={'YES' if is_mintable in ('1', '1.0') else 'no'}", |
| } |
|
|
| |
| buy_tax = float(goplus_data.get("buy_tax", "0")) |
| sell_tax = float(goplus_data.get("sell_tax", "0")) |
|
|
| if sell_tax > 50: |
| results["HP03"] = {"status": "fail", "details": f"Sell tax={sell_tax}%"} |
| elif sell_tax > 10: |
| results["HP03"] = {"status": "warning", "details": f"Sell tax={sell_tax}%"} |
|
|
| if buy_tax > 10 or sell_tax > 10: |
| results["TE02"] = { |
| "status": "warning" if sell_tax <= 50 else "fail", |
| "details": f"Buy={buy_tax}% Sell={sell_tax}%", |
| } |
|
|
| |
| transfer_pausable = goplus_data.get("transfer_pausable", "0") |
| results["HP04"] = { |
| "status": "warning" if transfer_pausable in ("1", "1.0") else "pass", |
| "details": f"Pausable={'YES' if transfer_pausable in ('1', '1.0') else 'no'}", |
| } |
|
|
| |
| owner_change_balance = goplus_data.get("owner_change_balance", "0") |
| results["CR04"] = { |
| "status": "warning" if owner_change_balance in ("1", "1.0") else "pass", |
| "details": "Owner can change balances" if owner_change_balance in ("1", "1.0") else "", |
| } |
|
|
| |
| is_blacklisted = goplus_data.get("is_blacklisted", "0") |
| results["TE04"] = { |
| "status": "warning" if is_blacklisted in ("1", "1.0") else "pass", |
| "details": f"Blacklist={'YES' if is_blacklisted in ('1', '1.0') else 'no'}", |
| } |
|
|
| |
| holder_count = int(goplus_data.get("holder_count", "0")) |
| results["HD03"] = { |
| "status": "warning" if holder_count < 10 else "pass", |
| "details": f"Holders={holder_count}", |
| } |
|
|
| |
| is_in_anti_whale = goplus_data.get("is_anti_whale", "0") |
| results["LR03"] = { |
| "status": "warning" if is_in_anti_whale in ("1", "1.0") else "pass", |
| "details": f"Anti-whale={'YES' if is_in_anti_whale in ('1', '1.0') else 'no'}", |
| } |
|
|
| except Exception as e: |
| logger.debug(f"GoPlus scan failed: {e}") |
|
|
| |
| |
| try: |
| api_key = kw.get("api_key", "") or os.getenv("HELIUS_API_KEY", "") or os.getenv("ALCHEMY_API_KEY", "") |
| if api_key and chain == "solana": |
| async with httpx.AsyncClient(timeout=10) as c: |
| r = await c.post( |
| f"https://mainnet.helius-rpc.com/?api-key={api_key}", |
| json={ |
| "jsonrpc": "2.0", |
| "id": 1, |
| "method": "getSignaturesForAddress", |
| "params": [address, {"limit": 1}], |
| }, |
| ) |
| if r.status_code == 200: |
| sigs = r.json().get("result", []) |
| if sigs: |
| block_time = sigs[0].get("blockTime", 0) |
| age_seconds = int(time.time()) - block_time |
| age_hours = age_seconds / 3600 |
| if age_hours < 1: |
| results["RP05"] = { |
| "status": "fail", |
| "details": f"Age={age_hours:.1f}h (<1h)", |
| } |
| elif age_hours < 24: |
| results["RP05"] = { |
| "status": "warning", |
| "details": f"Age={age_hours:.1f}h", |
| } |
| else: |
| results["RP05"] = {"status": "pass", "details": f"Age={age_hours:.0f}h"} |
| except Exception: |
| pass |
|
|
| |
| try: |
| volume_24h = float(kw.get("volume_24h", 0)) |
| liquidity = float(kw.get("liquidity_usd", 0)) |
| unique_wallets = int(kw.get("unique_wallets", 0)) |
| tx_count = int(kw.get("tx_count", 0)) |
|
|
| if volume_24h > 0 or liquidity > 0: |
| from app.databus.volume_authenticity import quick_authenticity_score |
|
|
| auth = quick_authenticity_score(volume_24h, liquidity, unique_wallets, tx_count) |
| fake_pct = auth.get("fake_volume_pct", 0) |
|
|
| if fake_pct > 50: |
| results["TA01"] = {"status": "fail", "details": f"Fake volume={fake_pct}%"} |
| elif fake_pct > 20: |
| results["TA01"] = {"status": "warning", "details": f"Fake volume={fake_pct}%"} |
|
|
| if liquidity > 0: |
| vl_ratio = volume_24h / liquidity |
| if vl_ratio > 5: |
| results["TA02"] = {"status": "warning", "details": f"V/L={vl_ratio:.1f}x"} |
| except Exception: |
| pass |
|
|
| |
| score, band, color = TokenSecurityScorer.compute_score(results) |
|
|
| return { |
| "checks": dict(results.items()), |
| "total_checks_run": len(results), |
| "security_score": score, |
| "risk_band": band, |
| "risk_color": color, |
| "scan_tier": 1, |
| "scan_type": "quick", |
| "source": "token_security_matrix", |
| } |
|
|
|
|
| |
|
|
|
|
| async def run_full_scan(address: str = "", chain: str = "ethereum", **kw) -> dict | None: |
| """DataBus provider: Full token security scan. |
| |
| Returns scored results with breakdown by category. |
| """ |
| if not address: |
| return None |
|
|
| cache_key = f"token_security:{chain}:{address}" |
| try: |
| r = redis.Redis( |
| host=REDIS_HOST, |
| port=REDIS_PORT, |
| password=REDIS_PASSWORD, |
| decode_responses=True, |
| socket_connect_timeout=2, |
| ) |
| cached = r.get(cache_key) |
| if cached: |
| r.close() |
| return json.loads(cached) |
| r.close() |
| except Exception: |
| pass |
|
|
| |
| result = await run_quick_scan(address, chain, **kw) |
|
|
| |
| result["token_address"] = address |
| result["chain"] = chain |
| result["scan_timestamp"] = datetime.now(UTC).isoformat() |
|
|
| |
| categories = defaultdict(lambda: {"pass": 0, "fail": 0, "warning": 0, "unknown": 0}) |
| for check_id, check_result in result["checks"].items(): |
| check = next((c for c in SECURITY_CHECKS if c.id == check_id), None) |
| cat = check.category if check else "unknown" |
| status = check_result.get("status", "unknown") |
| categories[cat][status] = categories[cat].get(status, 0) + 1 |
|
|
| result["category_breakdown"] = dict(categories) |
|
|
| |
| try: |
| r = redis.Redis( |
| host=REDIS_HOST, |
| port=REDIS_PORT, |
| password=REDIS_PASSWORD, |
| decode_responses=True, |
| socket_connect_timeout=2, |
| ) |
| r.setex(cache_key, CACHE_TTL, json.dumps(result, default=str)) |
| r.close() |
| except Exception: |
| pass |
|
|
| return result |
|
|
|
|
| async def get_check_matrix_endpoint(**kw) -> dict: |
| """Returns the full security check matrix definition.""" |
| return get_check_matrix() |
|
|