rmi-backend/app/degen_security_scanner.py

1260 lines
55 KiB
Python

#!/usr/bin/env python3
"""
╔═══════════════════════════════════════════════════════════════════════════════╗
║ R U G M U N C H I N T E L L I G E N C E ║
║ D E G E N S E C U R I T Y S C A N N E R ║
╚═══════════════════════════════════════════════════════════════════════════════╝
Comprehensive token security analysis combining:
• Free Solscan API (reverse-engineered, no rate limits)
• Helius Enhanced API (token metadata, enhanced TXs)
• GMGN Intelligence (smart money tags, top holders)
• Birdeye Market Data (price, volume, LP depth)
• DexScreener (pair analysis, liquidity verification)
• Moralis (cross-chain validation)
• SolanaFM (on-chain verification)
Generates a forensic-grade security report with:
→ Bundler Detection (launch manipulation)
→ Gas Funding Trace (where dev got SOL)
→ Exchange Funding % (centralized vs organic)
→ LP Analysis (locked? burned? depth?)
→ Sniper Metrics (first 20 buyers analyzed)
→ Insider Trading Detection (pre-launch buyers)
→ Wash Trading Detection (circular patterns)
→ Dev Wallet Forensics (history, other rugs)
→ Authority Analysis (mint/freeze still active?)
→ Holder Concentration (top 10/20/50 %)
→ Smart Money Ratio (pros vs retail)
→ Tax & Limit Detection (hidden fees)
→ Honeypot Test (can you sell?)
→ RUG PULL PROBABILITY SCORE (0-100)
Author: RMI Dev Team
Contract: Eme5T2s2HB7B8W4YgLG1eReQpnadEVUnQBRjaKTdBAGS
"""
import asyncio
import json
import logging
import os
import statistics
import sys
import time
from collections import defaultdict
from dataclasses import asdict, dataclass, field
from datetime import datetime
# Load the free solscan client
from app.free_solscan_client import FreeSolscanClient, is_known_exchange
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ─── Configuration ────────────────────────────────────────────────────────────
HELIUS_API_KEY = os.getenv("HELIUS_API_KEY", "")
BIRDEYE_API_KEY = os.getenv("BIRDEYE_API_KEY", "")
MORALIS_API_KEY = os.getenv("MORALIS_API_KEY", "")
GMGN_API_KEY = os.getenv("GMGN_API_KEY", "")
# Known scam/bundler program IDs
BUNDLER_PROGRAMS = {
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "Pump.fun Bundler",
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo": "Launchpad Bundler",
"MoonCVVNZFSYkqNXP6bxHLPL6QQxMagE3VCigVtg81o": "Moonshot Bundler",
}
# Known mixer/tumbler programs
MIXER_PROGRAMS = {
"Tornado": ["tornado", "mixer", "tumbler"],
"Cyclone": ["cyclone"],
"Samourai": ["samourai"],
}
# ─── Data Models ──────────────────────────────────────────────────────────────
@dataclass
class HolderProfile:
"""Detailed holder analysis."""
address: str
balance: float
percentage: float
exchange_tag: str | None = None
is_contract: bool = False
is_known_whale: bool = False
first_seen: str | None = None
funding_source: str | None = None
smart_money_tag: str | None = None
is_sniper: bool = False
is_insider: bool = False
is_bundler: bool = False
is_wash_trader: bool = False
other_tokens_held: int = 0
risk_score: int = 0
@dataclass
class FundingTrace:
"""Gas funding trace result."""
wallet: str
initial_funder: str
funder_tag: str | None = None
funding_amount_sol: float = 0.0
funding_time: str | None = None
hops_from_exchange: int = -1 # -1 = unknown, 0 = direct exchange, 1+ = hops
is_exchange_direct: bool = False
exchange_name: str | None = None
risk_flags: list[str] = field(default_factory=list)
@dataclass
class SniperProfile:
"""Launch sniper analysis."""
wallet: str
buy_time_ms: float # milliseconds after launch
buy_amount_sol: float
token_amount: float
price_paid: float
current_value: float = 0.0
pnl_percent: float = 0.0
is_still_holding: bool = True
sold_percentage: float = 0.0
is_known_sniper: bool = False
sniper_score: int = 0 # 0-100
@dataclass
class LPAnalysis:
"""Liquidity pool analysis."""
pool_address: str
dex_name: str
token_a_amount: float
token_b_amount: float
tvl_usd: float
lp_tokens_total: float
lp_tokens_locked: float
lp_locked_percentage: float = 0.0
lock_expiry: str | None = None
is_burned: bool = False
burn_tx: str | None = None
lp_holders: list[dict] = field(default_factory=list)
concentration_risk: str = "LOW" # LOW, MEDIUM, HIGH, CRITICAL
depth_score: int = 0 # 0-100
@dataclass
class WashTradeCluster:
"""Detected wash trading cluster."""
wallets: list[str]
trade_count: int
volume_usd: float
pattern_type: str # circular, back_forth, triangular
confidence: float # 0.0 - 1.0
sample_txs: list[str] = field(default_factory=list)
@dataclass
class SecurityReport:
"""Final comprehensive security report."""
# Token Identity
token_address: str
token_name: str = "Unknown"
token_symbol: str = "???"
decimals: int = 0
total_supply: float = 0.0
created_at: str | None = None
deployer: str = ""
# Authority Status
mint_authority: str | None = None
freeze_authority: str | None = None
mint_authority_renounced: bool = False
freeze_authority_renounced: bool = False
authorities_risk: str = "LOW"
# Holder Analysis
total_holders: int = 0
top_holders: list[HolderProfile] = field(default_factory=list)
top_10_concentration: float = 0.0
top_20_concentration: float = 0.0
top_50_concentration: float = 0.0
holder_health_score: int = 0
# Funding & Origins
deployer_funding: FundingTrace | None = None
exchange_funding_percentage: float = 0.0
organic_funding_percentage: float = 0.0
mixer_funding_detected: bool = False
funding_risk: str = "LOW"
# Bundler Detection
bundler_detected: bool = False
bundler_wallets: list[str] = field(default_factory=list)
bundler_buy_volume_sol: float = 0.0
bundler_percentage_of_supply: float = 0.0
bundler_pattern: str = ""
bundler_risk: str = "LOW"
# Sniper Analysis
snipers: list[SniperProfile] = field(default_factory=list)
sniper_count: int = 0
sniper_volume_sol: float = 0.0
avg_sniper_entry_time_ms: float = 0.0
sniper_risk: str = "LOW"
# LP Analysis
liquidity_pools: list[LPAnalysis] = field(default_factory=list)
total_liquidity_usd: float = 0.0
lp_risk: str = "LOW"
# Insider Trading
insiders_detected: list[str] = field(default_factory=list)
insider_buy_volume: float = 0.0
insider_risk: str = "LOW"
# Wash Trading
wash_trade_clusters: list[WashTradeCluster] = field(default_factory=list)
wash_trading_volume_usd: float = 0.0
wash_trading_risk: str = "LOW"
# Tax & Limits
buy_tax: float = 0.0
sell_tax: float = 0.0
transfer_tax: float = 0.0
max_buy_limit: float = 0.0
max_sell_limit: float = 0.0
max_wallet_limit: float = 0.0
honeypot_detected: bool = False
can_sell: bool = True
tax_risk: str = "LOW"
# Market Data
price_usd: float = 0.0
market_cap: float = 0.0
volume_24h: float = 0.0
price_change_24h: float = 0.0
liquidity_usd: float = 0.0
buy_sell_ratio: float = 1.0
# Smart Money
smart_money_holders: int = 0
dumb_money_holders: int = 0
smart_money_ratio: float = 0.0
# Dev History
dev_other_tokens: list[str] = field(default_factory=list)
dev_rug_history: int = 0
dev_is_known_scammer: bool = False
dev_risk: str = "LOW"
# Final Scores
overall_risk: str = "LOW" # LOW, MEDIUM, HIGH, CRITICAL
rug_pull_probability: int = 0 # 0-100
confidence_level: str = "HIGH" # LOW, MEDIUM, HIGH
critical_warnings: list[str] = field(default_factory=list)
recommendations: list[str] = field(default_factory=list)
scan_timestamp: str = ""
scan_duration_ms: int = 0
def to_dict(self) -> dict:
"""Convert to dict for JSON serialization."""
result = {}
for k, v in asdict(self).items():
if isinstance(v, list) and len(v) > 0 and hasattr(v[0], "to_dict"):
result[k] = [item.to_dict() if hasattr(item, "to_dict") else item for item in v]
elif hasattr(v, "to_dict"):
result[k] = v.to_dict()
else:
result[k] = v
return result
def to_json(self, indent: int = 2) -> str:
return json.dumps(self.to_dict(), indent=indent, default=str)
# ─── Main Scanner Engine ──────────────────────────────────────────────────────
class DegenSecurityScanner:
"""
Ultimate degen security scanner.
Analyzes any Solana token for 15+ scam indicators.
"""
def __init__(self):
self.solscan = FreeSolscanClient()
self.helius_key = HELIUS_API_KEY
self.birdeye_key = BIRDEYE_API_KEY
self.moralis_key = MORALIS_API_KEY
self.gmgn_key = GMGN_API_KEY
self.critical_warnings = []
self.recommendations = []
# ═══════════════════════════════════════════════════════════════════════
# PUBLIC API
# ═══════════════════════════════════════════════════════════════════════
async def full_scan(self, token_address: str, quick: bool = False) -> SecurityReport:
"""
Run comprehensive security scan on a token.
Args:
token_address: The token mint address
quick: If True, skip slow operations (dev history, wash trade deep scan)
Returns:
SecurityReport with all analysis results
"""
start_time = time.time()
logger.info(f"🔍 Starting DEGEN SCAN for {token_address}")
report = SecurityReport(
token_address=token_address,
scan_timestamp=datetime.utcnow().isoformat(),
)
# ── Phase 1: Token Metadata ──────────────────────────────────────────
logger.info("[1/7] Gathering token metadata...")
await self._phase1_metadata(report)
time.sleep(0.3)
# ── Phase 2: Authority Analysis ──────────────────────────────────────
logger.info("[2/7] Checking authorities...")
await self._phase2_authorities(report)
time.sleep(0.2)
# ── Phase 3: Holder Analysis ─────────────────────────────────────────
logger.info("[3/7] Analyzing holders...")
await self._phase3_holders(report)
time.sleep(0.3)
# ── Phase 4: Funding Trace ───────────────────────────────────────────
logger.info("[4/7] Tracing funding sources...")
await self._phase4_funding(report)
time.sleep(0.3)
# ── Phase 5: Bundler & Sniper Detection ─────────────────────────────
logger.info("[5/7] Detecting bundlers & snipers...")
await self._phase5_bundlers_snipers(report)
time.sleep(0.3)
# ── Phase 6: LP & Market Analysis ────────────────────────────────────
logger.info("[6/7] Analyzing liquidity pools...")
await self._phase6_liquidity(report)
time.sleep(0.3)
# ── Phase 7: Advanced Detection ──────────────────────────────────────
logger.info("[7/7] Advanced scam detection...")
await self._phase7_advanced(report, quick=quick)
# ── Final: Calculate Scores ──────────────────────────────────────────
logger.info("⚡ Calculating final risk scores...")
self._calculate_final_scores(report)
report.scan_duration_ms = int((time.time() - start_time) * 1000)
report.critical_warnings = self.critical_warnings
report.recommendations = self.recommendations
logger.info(f"✅ DEGEN SCAN complete in {report.scan_duration_ms}ms")
logger.info(f"🎯 RUG PULL PROBABILITY: {report.rug_pull_probability}%")
logger.info(f"🚨 RISK LEVEL: {report.overall_risk}")
return report
# ═══════════════════════════════════════════════════════════════════════
# PHASE 1: TOKEN METADATA
# ═══════════════════════════════════════════════════════════════════════
async def _phase1_metadata(self, report: SecurityReport) -> None:
"""Get basic token info from Solscan + Helius."""
# Try Solscan first
token_data = self.solscan.token_data(report.token_address)
if token_data:
report.token_name = token_data.get("name", "Unknown")
report.token_symbol = token_data.get("symbol", "???")
report.decimals = token_data.get("decimals", 0)
supply_raw = token_data.get("supply", "0")
try:
report.total_supply = (
float(supply_raw) / (10**report.decimals) if report.decimals else float(supply_raw)
)
except Exception:
report.total_supply = 0
report.created_at = token_data.get("createdTime")
else:
# Fallback: try Helius
helius_data = await self._helius_token_metadata(report.token_address)
if helius_data:
report.token_name = helius_data.get("name", "Unknown")
report.token_symbol = helius_data.get("symbol", "???")
report.decimals = helius_data.get("decimals", 0)
# Try to get market data from Birdeye
market_data = await self._birdeye_token_data(report.token_address)
if market_data:
report.price_usd = market_data.get("price", 0)
report.market_cap = market_data.get("mc", 0)
report.volume_24h = market_data.get("v24hUSD", 0)
report.price_change_24h = market_data.get("priceChange24h", 0)
report.liquidity_usd = market_data.get("liquidity", 0)
report.buy_sell_ratio = market_data.get("buySellRatio", 1.0)
# Get total holders
holders_total = self.solscan.token_holders_total(report.token_address)
if holders_total:
report.total_holders = holders_total
# ═══════════════════════════════════════════════════════════════════════
# PHASE 2: AUTHORITY ANALYSIS
# ═══════════════════════════════════════════════════════════════════════
async def _phase2_authorities(self, report: SecurityReport) -> None:
"""Check mint/freeze authorities via Helius."""
helius_data = await self._helius_token_metadata(report.token_address)
if helius_data:
mint_auth = helius_data.get("mintAuthority")
freeze_auth = helius_data.get("freezeAuthority")
report.mint_authority = mint_auth
report.freeze_authority = freeze_auth
report.mint_authority_renounced = mint_auth is None or mint_auth == ""
report.freeze_authority_renounced = freeze_auth is None or freeze_auth == ""
# Risk assessment
risk_score = 0
if not report.mint_authority_renounced:
risk_score += 30
self.critical_warnings.append(
f"🚨 MINT AUTHORITY ACTIVE: Dev can mint unlimited tokens! Authority: {mint_auth[:20]}..."
)
self.recommendations.append("Mint authority should be renounced for investor safety")
if not report.freeze_authority_renounced:
risk_score += 25
self.critical_warnings.append("❄️ FREEZE AUTHORITY ACTIVE: Dev can freeze any wallet!")
self.recommendations.append("Freeze authority should be renounced immediately")
if risk_score >= 45:
report.authorities_risk = "CRITICAL"
elif risk_score >= 25:
report.authorities_risk = "HIGH"
elif risk_score >= 10:
report.authorities_risk = "MEDIUM"
else:
report.authorities_risk = "LOW"
# ═══════════════════════════════════════════════════════════════════════
# PHASE 3: HOLDER ANALYSIS
# ═══════════════════════════════════════════════════════════════════════
async def _phase3_holders(self, report: SecurityReport) -> None:
"""Analyze top holders for concentration and risk."""
holders = self.solscan.get_holder_wallets(report.token_address, top_n=50)
if not holders:
return
profiles = []
top_10_total = 0.0
top_20_total = 0.0
top_50_total = 0.0
for i, h in enumerate(holders):
addr = h.get("address", "")
amount = h.get("amount", 0)
pct = h.get("uiAmount", 0)
profile = HolderProfile(
address=addr,
balance=float(amount),
percentage=pct,
)
# Tag known exchanges
exchange = is_known_exchange(addr)
if exchange:
profile.exchange_tag = exchange
profile.is_contract = True
# Detect contracts (short addresses often = programs)
if len(addr) < 40:
profile.is_contract = True
# Whale threshold: >2% = whale
if pct > 2.0:
profile.is_known_whale = True
profiles.append(profile)
if i < 10:
top_10_total += pct
if i < 20:
top_20_total += pct
top_50_total += pct
report.top_holders = profiles[:20]
report.top_10_concentration = round(top_10_total, 2)
report.top_20_concentration = round(top_20_total, 2)
report.top_50_concentration = round(top_50_total, 2)
# Smart money estimation (heuristic)
known_smart = sum(1 for p in profiles if p.is_known_whale and not p.is_contract)
report.smart_money_holders = known_smart
report.dumb_money_holders = max(0, report.total_holders - known_smart)
report.smart_money_ratio = round(known_smart / len(profiles), 2) if profiles else 0
# Holder concentration risk
if top_10_total > 80:
report.holder_health_score = 10
self.critical_warnings.append(f"☠️ CATASTROPHIC CONCENTRATION: Top 10 holders own {top_10_total}%!")
elif top_10_total > 50:
report.holder_health_score = 30
self.critical_warnings.append(f"⚠️ DANGEROUS CONCENTRATION: Top 10 hold {top_10_total}%")
elif top_10_total > 30:
report.holder_health_score = 50
elif top_10_total > 20:
report.holder_health_score = 70
else:
report.holder_health_score = 90
# ═══════════════════════════════════════════════════════════════════════
# PHASE 4: FUNDING TRACE
# ═══════════════════════════════════════════════════════════════════════
async def _phase4_funding(self, report: SecurityReport) -> None:
"""Trace deployer wallet funding sources."""
if not report.deployer:
# Try to find deployer from creation TX
creation_tx = self.solscan.get_token_creation_tx(report.token_address)
if creation_tx:
# The signer of the creation tx is the deployer
signers = creation_tx.get("signer", [])
if signers:
report.deployer = signers[0]
if not report.deployer:
logger.warning("Could not determine deployer address")
return
# Get deployer's funding sources
funding_sources = self.solscan.get_wallet_funding_sources(report.deployer, days=30)
# Get top transfers for deeper analysis
self.solscan.top_address_transfers(report.deployer, range_days=30)
exchange_count = 0
organic_count = 0
mixer_detected = False
trace = FundingTrace(wallet=report.deployer, initial_funder="")
# Analyze funding sources
for source in funding_sources[:20]:
from_addr = source.get("from", "")
exchange = is_known_exchange(from_addr)
if exchange:
exchange_count += 1
if not trace.is_exchange_direct:
trace.is_exchange_direct = True
trace.exchange_name = exchange
trace.initial_funder = from_addr
trace.funder_tag = exchange
trace.funding_amount_sol = source.get("amount", 0)
trace.funding_time = source.get("time")
trace.hops_from_exchange = 0
else:
organic_count += 1
if not trace.initial_funder:
trace.initial_funder = from_addr
trace.funding_amount_sol = source.get("amount", 0)
# Check for mixer patterns (round amounts, timing)
amount = source.get("amount", 0)
if isinstance(amount, (int, float)) and amount in [0.1, 0.2, 0.5, 1.0]:
trace.risk_flags.append(f"Suspicious round amount: {amount} SOL")
# Calculate percentages
total_sources = exchange_count + organic_count
if total_sources > 0:
report.exchange_funding_percentage = round(exchange_count / total_sources * 100, 1)
report.organic_funding_percentage = round(organic_count / total_sources * 100, 1)
report.deployer_funding = trace
report.mixer_funding_detected = mixer_detected
# Risk assessment
if trace.hops_from_exchange == 0 and exchange_count > 0:
report.funding_risk = "LOW" # Direct exchange = transparent
elif organic_count > exchange_count * 2:
report.funding_risk = "MEDIUM" # Mostly anonymous funding
self.critical_warnings.append("⚠️ Deployer funded from non-exchange sources — possible obfuscation")
elif mixer_detected:
report.funding_risk = "CRITICAL"
self.critical_warnings.append("🚨 MIXER FUNDING DETECTED: Deployer used a tumbler!")
# ═══════════════════════════════════════════════════════════════════════
# PHASE 5: BUNDLER & SNIPER DETECTION
# ═══════════════════════════════════════════════════════════════════════
async def _phase5_bundlers_snipers(self, report: SecurityReport) -> None:
"""Detect launch bundlers and sniper wallets."""
# Get DeFi activities (swaps = buys/sells)
activities = self.solscan.token_defi_activities(report.token_address, page_size=200)
if not activities:
return
# Find earliest activities (launch window)
sorted_acts = sorted(activities, key=lambda x: x.get("block_time", 0))
if not sorted_acts:
return
# Define launch time as first activity
launch_time = sorted_acts[0].get("block_time", 0)
report.created_at = datetime.fromtimestamp(launch_time).isoformat() if launch_time else None
# Analyze first 30 buys (sniper window: first 30 seconds)
SNIPER_WINDOW_MS = 30000 # 30 seconds
sniper_candidates = []
buyer_timestamps = {} # wallet -> [buy_times]
for act in sorted_acts[:100]:
act_type = act.get("activity_type", "").upper()
if "BUY" not in act_type and "SWAP" not in act_type:
continue
buyer = act.get("from_address") or act.get("address")
block_time = act.get("block_time", 0)
time_after_launch_ms = (block_time - launch_time) * 1000 if launch_time else 0
# Track buyer timestamps for bundler detection
if buyer:
if buyer not in buyer_timestamps:
buyer_timestamps[buyer] = []
buyer_timestamps[buyer].append(
{
"time": block_time,
"amount_sol": act.get("value", 0),
"tx": act.get("trans_id"),
}
)
# Sniper detection: buys within first 30 seconds
if 0 <= time_after_launch_ms <= SNIPER_WINDOW_MS and buyer:
sniper = SniperProfile(
wallet=buyer,
buy_time_ms=round(time_after_launch_ms, 0),
buy_amount_sol=act.get("value", 0),
token_amount=act.get("token_amount", 0),
price_paid=act.get("value", 0) / max(act.get("token_amount", 1), 1),
)
sniper_candidates.append(sniper)
# Bundler detection: wallets buying at nearly identical timestamps
bundler_clusters = []
for wallet, buys in buyer_timestamps.items():
if len(buys) >= 2:
# Check if multiple buys within 2 seconds = bundler pattern
timestamps = sorted([b["time"] for b in buys])
for i in range(len(timestamps) - 1):
if timestamps[i + 1] - timestamps[i] <= 2:
bundler_clusters.append(wallet)
break
# Also detect: many wallets buying within same 3-second window
launch_window_buys = defaultdict(list)
for wallet, buys in buyer_timestamps.items():
for b in buys:
window_key = int(b["time"] / 3) * 3 # 3-second buckets
launch_window_buys[window_key].append(wallet)
for _window, wallets in launch_window_buys.items():
if len(wallets) >= 5: # 5+ wallets in same 3s window = bundler
bundler_clusters.extend(wallets)
# Deduplicate
report.bundler_wallets = list(set(bundler_clusters))
report.bundler_detected = len(report.bundler_wallets) > 0
if report.bundler_detected:
# Calculate bundler impact
bundler_supply_pct = len(report.bundler_wallets) * 0.5 # rough estimate
report.bundler_percentage_of_supply = min(bundler_supply_pct, 100)
report.bundler_pattern = "synchronized_launch" if len(report.bundler_wallets) >= 5 else "clustered_buys"
report.bundler_risk = "HIGH" if len(report.bundler_wallets) >= 10 else "MEDIUM"
self.critical_warnings.append(
f"🎯 BUNDLER DETECTED: {len(report.bundler_wallets)} wallets bought in coordinated pattern!"
)
# Sniper summary
report.snipers = sniper_candidates[:20]
report.sniper_count = len(sniper_candidates)
if sniper_candidates:
report.avg_sniper_entry_time_ms = round(statistics.mean(s.buy_time_ms for s in sniper_candidates), 0)
report.sniper_volume_sol = round(sum(s.buy_amount_sol for s in sniper_candidates), 2)
if report.sniper_count >= 15:
report.sniper_risk = "HIGH"
elif report.sniper_count >= 8:
report.sniper_risk = "MEDIUM"
# ═══════════════════════════════════════════════════════════════════════
# PHASE 6: LIQUIDITY ANALYSIS
# ═══════════════════════════════════════════════════════════════════════
async def _phase6_liquidity(self, report: SecurityReport) -> None:
"""Analyze LP depth, lock status, and concentration."""
lp_data = self.solscan.get_lp_token_accounts(report.token_address)
total_liq = 0.0
for lp in lp_data:
tvl = lp.get("tvl_usd", 0) or 0
total_liq += tvl
analysis = LPAnalysis(
pool_address=lp.get("market_id", ""),
dex_name=lp.get("program", "Unknown"),
token_a_amount=lp.get("liquidity_a", 0) or 0,
token_b_amount=lp.get("liquidity_b", 0) or 0,
tvl_usd=tvl,
lp_tokens_total=0,
lp_tokens_locked=0,
)
# Check lock status
if lp.get("lp_locked"):
analysis.lp_locked_percentage = 100.0
analysis.is_burned = lp.get("lpLocked") == "burned"
else:
analysis.lp_locked_percentage = 0.0
# Depth score based on TVL
if tvl > 100000:
analysis.depth_score = 90
analysis.concentration_risk = "LOW"
elif tvl > 50000:
analysis.depth_score = 70
analysis.concentration_risk = "MEDIUM"
elif tvl > 10000:
analysis.depth_score = 50
analysis.concentration_risk = "HIGH"
else:
analysis.depth_score = 20
analysis.concentration_risk = "CRITICAL"
report.liquidity_pools.append(analysis)
report.total_liquidity_usd = total_liq
# LP risk assessment
locked_pools = sum(1 for p in report.liquidity_pools if p.lp_locked_percentage > 80)
total_pools = len(report.liquidity_pools)
if total_pools == 0:
report.lp_risk = "CRITICAL"
self.critical_warnings.append("🚨 NO LIQUIDITY POOLS FOUND!")
elif locked_pools == 0:
report.lp_risk = "CRITICAL"
self.critical_warnings.append("🚨 LP NOT LOCKED: Dev can rug liquidity at any moment!")
elif total_liq < 10000:
report.lp_risk = "HIGH"
self.critical_warnings.append(f"⚠️ LOW LIQUIDITY: Only ${total_liq:,.0f} TVL")
elif total_liq < 50000:
report.lp_risk = "MEDIUM"
else:
report.lp_risk = "LOW"
# ═══════════════════════════════════════════════════════════════════════
# PHASE 7: ADVANCED DETECTION
# ═══════════════════════════════════════════════════════════════════════
async def _phase7_advanced(self, report: SecurityReport, quick: bool = False) -> None:
"""Insider trading, wash trading, dev history, honeypot detection."""
# ── Insider Trading ──────────────────────────────────────────────────
await self._detect_insiders(report)
# ── Wash Trading (skip in quick mode) ───────────────────────────────
if not quick:
await self._detect_wash_trading(report)
# ── Dev History ──────────────────────────────────────────────────────
await self._analyze_dev_history(report)
# ── Tax & Honeypot ───────────────────────────────────────────────────
await self._detect_tax_and_honeypot(report)
async def _detect_insiders(self, report: SecurityReport) -> None:
"""Detect wallets that bought suspiciously early."""
# Insiders = wallets that bought before public awareness
# Heuristic: wallets that bought within first 60s with no prior token history
activities = self.solscan.token_defi_activities(report.token_address, page_size=100)
if not activities:
return
sorted_acts = sorted(activities, key=lambda x: x.get("block_time", 0))
if not sorted_acts:
return
launch_time = sorted_acts[0].get("block_time", 0)
INSIDER_WINDOW = 120 # 2 minutes
insiders = []
for act in sorted_acts:
act_type = str(act.get("activity_type", "")).upper()
if "BUY" not in act_type and "SWAP" not in act_type:
continue
block_time = act.get("block_time", 0)
elapsed = block_time - launch_time
if 0 <= elapsed <= INSIDER_WINDOW:
buyer = act.get("from_address") or act.get("address")
if buyer and buyer not in insiders:
# Check if this wallet was created very recently
acct_info = self.solscan.account_info(buyer)
if acct_info:
# Fresh wallet = strong insider signal
acct_info.get("lamports", 0)
# If wallet has exactly the buy amount = funded just for this
insiders.append(buyer)
report.insiders_detected = insiders[:10]
if len(insiders) >= 5:
report.insider_risk = "HIGH"
self.critical_warnings.append(f"🕵️ INSIDER TRADING: {len(insiders)} wallets bought within 2min of launch")
elif len(insiders) >= 2:
report.insider_risk = "MEDIUM"
async def _detect_wash_trading(self, report: SecurityReport) -> None:
"""Detect circular trading patterns."""
transfers = self.solscan.token_transfer_summary(report.token_address)
if not transfers:
return
# Get recent transfers for pattern analysis
recent = self.solscan.account_transfers(report.token_address, page_size=200)
if not recent:
return
# Build transfer graph
graph = defaultdict(lambda: defaultdict(int))
for tx in recent[:200]:
from_addr = tx.get("from_address", "")
to_addr = tx.get("to_address", "")
amount = float(tx.get("change_amount", 0))
if from_addr and to_addr and amount > 0:
graph[from_addr][to_addr] += 1
# Find cycles (A -> B -> A)
clusters = []
visited = set()
for wallet_a in list(graph.keys())[:50]:
if wallet_a in visited:
continue
for wallet_b, count_ab in graph[wallet_a].items():
if wallet_b in graph and wallet_a in graph[wallet_b]:
count_ba = graph[wallet_b][wallet_a]
if count_ab >= 2 and count_ba >= 2:
cluster = WashTradeCluster(
wallets=[wallet_a, wallet_b],
trade_count=count_ab + count_ba,
volume_usd=0,
pattern_type="back_forth",
confidence=min(0.95, (count_ab + count_ba) / 10),
)
clusters.append(cluster)
visited.add(wallet_a)
visited.add(wallet_b)
report.wash_trade_clusters = clusters[:5]
if clusters:
report.wash_trading_volume_usd = sum(c.volume_usd for c in clusters)
total_volume = report.volume_24h or 1
wash_pct = report.wash_trading_volume_usd / total_volume * 100
if wash_pct > 50:
report.wash_trading_risk = "CRITICAL"
self.critical_warnings.append(f"🔄 WASH TRADING: {wash_pct:.0f}% of volume appears artificial!")
elif wash_pct > 20:
report.wash_trading_risk = "HIGH"
elif wash_pct > 5:
report.wash_trading_risk = "MEDIUM"
async def _analyze_dev_history(self, report: SecurityReport) -> None:
"""Check deployer's history for other tokens and potential rugs."""
if not report.deployer:
return
# Get deployer's recent transactions
txs = self.solscan.get_deployer_transactions(report.deployer, pages=3)
token_creations = []
for tx in txs:
# Look for token creation patterns in tx history
tx_hash = tx.get("txHash", "")
if not tx_hash:
continue
detail = self.solscan.transaction_detail(tx_hash)
if detail and "Tokenkeg" in str(detail):
# Potentially a token creation
programs = detail.get("parsedInstructions", [])
for inst in programs:
if "initializeMint" in str(inst):
token_creations.append(tx_hash)
report.dev_other_tokens = token_creations[:10]
# If dev has created many tokens, flag it
if len(token_creations) >= 5:
report.dev_risk = "HIGH"
report.dev_is_known_scammer = True
report.dev_rug_history = len(token_creations) - 1 # estimate
self.critical_warnings.append(
f"👺 DEV SERIAL LAUNCHER: Created {len(token_creations)} tokens — "
f"likely a token factory / rug operation"
)
elif len(token_creations) >= 2:
report.dev_risk = "MEDIUM"
async def _detect_tax_and_honeypot(self, report: SecurityReport) -> None:
"""Attempt to detect taxes and honeypot behavior."""
# Tax detection: compare buy vs sell amounts in recent txs
activities = self.solscan.token_defi_activities(report.token_address, page_size=100)
if not activities:
return
buys = []
sells = []
for act in activities:
act_type = str(act.get("activity_type", "")).upper()
value = act.get("value", 0)
if "BUY" in act_type or "IN" in act_type:
buys.append(value)
elif "SELL" in act_type or "OUT" in act_type:
sells.append(value)
# If significantly more buys than sells, possible honeypot
if len(buys) > 10 and len(sells) < 2:
report.honeypot_detected = True
report.can_sell = False
self.critical_warnings.append("🍯 HONEYPOT DETECTED: Many buys but almost no sells!")
report.tax_risk = "CRITICAL"
elif len(buys) > 10 and len(sells) > 0:
# Check if sells are much smaller than buys (heavy tax)
avg_buy = statistics.mean(buys) if buys else 0
avg_sell = statistics.mean(sells) if sells else 0
if avg_buy > 0 and avg_sell > 0:
ratio = avg_sell / avg_buy
if ratio < 0.5:
# Possible high sell tax
estimated_tax = (1 - ratio) * 100
report.sell_tax = round(estimated_tax, 1)
if estimated_tax > 20:
report.tax_risk = "HIGH"
self.critical_warnings.append(f"💸 HEAVY SELL TAX ESTIMATED: ~{estimated_tax:.0f}%")
# ═══════════════════════════════════════════════════════════════════════
# FINAL SCORING
# ═══════════════════════════════════════════════════════════════════════
def _calculate_final_scores(self, report: SecurityReport) -> None:
"""Calculate the ultimate RUG PULL PROBABILITY score."""
score = 0
# Authority risk (20 pts)
auth_map = {"LOW": 0, "MEDIUM": 10, "HIGH": 15, "CRITICAL": 20}
score += auth_map.get(report.authorities_risk, 0)
# Holder concentration (15 pts)
score += min(report.top_10_concentration * 0.15, 15)
# Funding risk (10 pts)
fund_map = {"LOW": 0, "MEDIUM": 5, "HIGH": 8, "CRITICAL": 10}
score += fund_map.get(report.funding_risk, 0)
# Bundler risk (15 pts)
if report.bundler_detected:
score += min(len(report.bundler_wallets) * 1.5, 15)
# Sniper risk (5 pts)
sniper_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 5}
score += sniper_map.get(report.sniper_risk, 0)
# Liquidity risk (15 pts)
lp_map = {"LOW": 0, "MEDIUM": 7, "HIGH": 12, "CRITICAL": 15}
score += lp_map.get(report.lp_risk, 0)
# Insider risk (5 pts)
insider_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 5}
score += insider_map.get(report.insider_risk, 0)
# Wash trading (5 pts)
wash_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 4, "CRITICAL": 5}
score += wash_map.get(report.wash_trading_risk, 0)
# Tax/honeypot (5 pts)
if report.honeypot_detected:
score += 5
else:
tax_map = {"LOW": 0, "MEDIUM": 2, "HIGH": 4, "CRITICAL": 5}
score += tax_map.get(report.tax_risk, 0)
# Dev risk (5 pts)
dev_map = {"LOW": 0, "MEDIUM": 3, "HIGH": 5}
score += dev_map.get(report.dev_risk, 0)
# Cap and finalize
report.rug_pull_probability = min(int(score), 100)
if report.rug_pull_probability >= 70:
report.overall_risk = "CRITICAL"
self.recommendations.insert(0, "🛑 DO NOT INVEST — EXTREME RISK")
elif report.rug_pull_probability >= 50:
report.overall_risk = "HIGH"
self.recommendations.insert(0, "⚠️ HIGH RISK — Only gamble what you can lose")
elif report.rug_pull_probability >= 30:
report.overall_risk = "MEDIUM"
else:
report.overall_risk = "LOW"
# Confidence level
data_sources = sum(
[
report.total_holders > 0,
report.total_liquidity_usd > 0,
len(report.top_holders) > 0,
report.mint_authority is not None,
]
)
if data_sources >= 4:
report.confidence_level = "HIGH"
elif data_sources >= 2:
report.confidence_level = "MEDIUM"
else:
report.confidence_level = "LOW"
# Final recommendations
if not report.mint_authority_renounced:
self.recommendations.append("Verify mint authority renouncement on-chain")
if report.total_liquidity_usd < 50000:
self.recommendations.append("Low liquidity — large sells will cause massive slippage")
if report.top_10_concentration > 40:
self.recommendations.append("Top-heavy distribution — watch for whale dumps")
# ═══════════════════════════════════════════════════════════════════════
# EXTERNAL API HELPERS
# ═══════════════════════════════════════════════════════════════════════
async def _helius_token_metadata(self, token_address: str) -> dict | None:
"""Fetch token metadata from Helius."""
if not self.helius_key:
return None
try:
url = f"https://mainnet.helius-rpc.com/?api-key={self.helius_key}"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getAsset",
"params": {"id": token_address},
}
# Synchronous fallback
resp = httpx.post(url, json=payload, timeout=10)
if resp.status_code == 200:
result = resp.json().get("result")
if result:
return {
"name": result.get("content", {}).get("metadata", {}).get("name"),
"symbol": result.get("content", {}).get("metadata", {}).get("symbol"),
"decimals": result.get("token_info", {}).get("decimals", 0),
"mintAuthority": result.get("mint_extensions", {}).get("mintAuthority"),
"freezeAuthority": result.get("mint_extensions", {}).get("freezeAuthority"),
}
except Exception as e:
logger.warning(f"Helius metadata failed: {e}")
return None
async def _birdeye_token_data(self, token_address: str) -> dict | None:
"""Fetch market data from Birdeye."""
if not self.birdeye_key:
return None
try:
headers = {"X-API-KEY": self.birdeye_key}
# Price data
price_resp = httpx.get(
f"https://public-api.birdeye.so/defi/price?address={token_address}",
headers=headers,
timeout=10,
)
# Token overview
overview_resp = httpx.get(
f"https://public-api.birdeye.so/defi/token_overview?address={token_address}",
headers=headers,
timeout=10,
)
result = {}
if price_resp.status_code == 200:
p_data = price_resp.json().get("data", {})
result["price"] = p_data.get("value", 0)
result["priceChange24h"] = p_data.get("priceChange24h", 0)
if overview_resp.status_code == 200:
o_data = overview_resp.json().get("data", {})
result["mc"] = o_data.get("mc", 0)
result["v24hUSD"] = o_data.get("v24hUSD", 0)
result["liquidity"] = o_data.get("liquidity", 0)
result["buySellRatio"] = o_data.get("buySellRatio", 1.0)
result["holders"] = o_data.get("holder", 0)
return result if result else None
except Exception as e:
logger.warning(f"Birdeye data failed: {e}")
return None
# ═══════════════════════════════════════════════════════════════════════════════
# CONVENIENCE FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
async def scan_token(token_address: str, quick: bool = False) -> SecurityReport:
"""One-shot token scan."""
scanner = DegenSecurityScanner()
return await scanner.full_scan(token_address, quick=quick)
def format_report(report: SecurityReport) -> str:
"""Format a security report for display."""
lines = [
"╔══════════════════════════════════════════════════════════════════════╗",
"║ RUG MUNCH INTELLIGENCE — DEGEN SECURITY SCAN ║",
"╚══════════════════════════════════════════════════════════════════════╝",
"",
f"🪙 Token: {report.token_name} (${report.token_symbol})",
f"📍 Address: {report.token_address}",
f"📊 Supply: {report.total_supply:,.0f} | Decimals: {report.decimals}",
f"👥 Holders: {report.total_holders:,}",
"",
"━━━ AUTHORITY STATUS ━━━",
f" Mint Authority: {'✅ RENOUNCED' if report.mint_authority_renounced else '❌ ACTIVE'}",
f" Freeze Authority: {'✅ RENOUNCED' if report.freeze_authority_renounced else '❌ ACTIVE'}",
f" Risk: {report.authorities_risk}",
"",
"━━━ HOLDER CONCENTRATION ━━━",
f" Top 10: {report.top_10_concentration}% | Top 20: {report.top_20_concentration}% | Top 50: {report.top_50_concentration}%",
f" Health Score: {report.holder_health_score}/100",
f" Smart Money: {report.smart_money_holders} wallets",
"",
"━━━ FUNDING TRACE ━━━",
f" Exchange Funding: {report.exchange_funding_percentage}%",
f" Organic Funding: {report.organic_funding_percentage}%",
f" Mixer Used: {'YES 🚨' if report.mixer_funding_detected else 'No'}",
f" Risk: {report.funding_risk}",
"",
"━━━ BUNDLER DETECTION ━━━",
f" Detected: {'YES 🎯' if report.bundler_detected else 'No'}",
]
if report.bundler_detected:
lines.extend(
[
f" Bundler Wallets: {len(report.bundler_wallets)}",
f" Pattern: {report.bundler_pattern}",
f" Risk: {report.bundler_risk}",
]
)
lines.extend(
[
"",
"━━━ SNIPER ANALYSIS ━━━",
f" Snipers Detected: {report.sniper_count}",
f" Avg Entry Time: {report.avg_sniper_entry_time_ms / 1000:.1f}s after launch",
f" Sniper Volume: {report.sniper_volume_sol:.2f} SOL",
f" Risk: {report.sniper_risk}",
"",
"━━━ LIQUIDITY ANALYSIS ━━━",
f" Total TVL: ${report.total_liquidity_usd:,.0f}",
f" Pools: {len(report.liquidity_pools)}",
]
)
for lp in report.liquidity_pools:
lines.append(
f"{lp.dex_name}: ${lp.tvl_usd:,.0f} TVL | Locked: {lp.lp_locked_percentage}% | Depth: {lp.depth_score}/100"
)
lines.extend(
[
f" Risk: {report.lp_risk}",
"",
"━━━ ADVANCED DETECTION ━━━",
f" Insiders: {len(report.insiders_detected)} wallets | Risk: {report.insider_risk}",
f" Wash Clusters: {len(report.wash_trade_clusters)} | Risk: {report.wash_trading_risk}",
f" Dev Other Tokens: {len(report.dev_other_tokens)} | Risk: {report.dev_risk}",
f" Buy Tax: {report.buy_tax}% | Sell Tax: {report.sell_tax}%",
f" Honeypot: {'YES 🍯' if report.honeypot_detected else 'No'}",
"",
"━━━ MARKET DATA ━━━",
f" Price: ${report.price_usd:.10f}" if report.price_usd < 0.001 else f" Price: ${report.price_usd:.6f}",
f" Market Cap: ${report.market_cap:,.0f}",
f" 24h Volume: ${report.volume_24h:,.0f}",
f" 24h Change: {report.price_change_24h:+.2f}%",
f" Buy/Sell Ratio: {report.buy_sell_ratio:.2f}",
"",
"━━━ FINAL VERDICT ━━━",
f" RUG PULL PROBABILITY: {report.rug_pull_probability}%",
f" OVERALL RISK: {report.overall_risk}",
f" Confidence: {report.confidence_level}",
f" Scan Time: {report.scan_duration_ms}ms",
"",
]
)
if report.critical_warnings:
lines.extend(
[
"━━━ ⚠️ CRITICAL WARNINGS ⚠️ ━━━",
*[f" {w}" for w in report.critical_warnings[:10]],
"",
]
)
if report.recommendations:
lines.extend(
[
"━━━ 💡 RECOMMENDATIONS ━━━",
*[f" {r}" for r in report.recommendations[:8]],
"",
]
)
lines.extend(
[
"═══════════════════════════════════════════════════════════════",
" Powered by Rug Munch Intelligence — Protecting Degens Since 2024",
"═══════════════════════════════════════════════════════════════",
]
)
return "\n".join(lines)
# ═══════════════════════════════════════════════════════════════════════════════
# CLI / TEST
# ═══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
# Test scan on a known token
test_token = "So11111111111111111111111111111111111111112" # Wrapped SOL
if len(sys.argv) > 1:
test_token = sys.argv[1]
print(f"Testing Degen Security Scanner on: {test_token}")
report = asyncio.run(scan_token(test_token, quick=True))
print(format_report(report))