rmi-backend/app/smart_contract_honeypot_detector.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

1216 lines
50 KiB
Python

"""
Smart Contract Honeypot & Malicious Token Detector
===================================================
Comprehensive analysis of token contracts for honeypot traps, rug-pull
patterns, sell restrictions, hidden malicious functions, and upgrade risks.
What it detects:
1. Honeypot Mechanisms - Tokens that let you buy but not sell (high fees,
blacklists, cooldown timers, max sell limits, trading cooldowns)
2. Ownership/Admin Abuse - Owner-only mint functions, upgradable proxies
leading to malicious implementations, ownership renouncement status
3. Hidden Fee Structures - Buy/sell taxes, dynamic fees that change based
on time/volume/address, fee-to-owner sinks
4. Liquidity Trap Detection - Locked vs unlocked LP, liquidity pool drain
functions, fake burn mechanisms
5. Proxy & Upgrade Risks - EIP-1967/EIP-1822 proxy patterns, timelock
bypasses, unverified implementations
6. Blacklist/Whitelist Mechanisms - Address-level restrictions, trading
pauses, max wallet limits, anti-whale caps
7. Malicious Function Patterns - Selfdestruct traps, hidden transfer
functions, approve-based attacks, reentrancy vulnerabilities
Competitive advantage:
- Honeypot.is and TokenSniffer are paid/slow with limited chain coverage
- RugDoc focuses on specific chains and has delayed updates
- Our solution is free, cross-chain (EVM + Solana), and integrates with
existing RMI tooling (bridge_health_monitor, rug_imminence_predictor)
- Proactive pre-trade analysis - detect traps before buying, not after
- Uses multiple data sources with fallback for maximum coverage
Usage:
from app.smart_contract_honeypot_detector import HoneypotDetector
detector = HoneypotDetector()
result = await detector.analyze_token("0x...", chain="ethereum")
print(result.risk_score)
for finding in result.findings:
print(finding.severity, finding.description)
CLI:
python3 smart_contract_honeypot_detector.py 0x... --chain ethereum
python3 smart_contract_honeypot_detector.py 0x... --deep-scan
python3 smart_contract_honeypot_detector.py 0x... --format json
"""
import asyncio
import json
import logging
import os
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import Any
logger = logging.getLogger(__name__)
# ═══════════════════════════════════════════════════════════════════
# Enums & Types
# ═══════════════════════════════════════════════════════════════════
class FindingSeverity(Enum):
"""Severity level for security findings."""
CRITICAL = "critical" # Definitely a scam/honeypot - do not trade
HIGH = "high" # Strongly suspicious - high risk of trap
MEDIUM = "medium" # Potentially risky - investigate further
LOW = "low" # Minor concern - informational
INFO = "info" # Informational observation
class HoneypotType(Enum):
"""Classification of detected honeypot mechanisms."""
SELL_RESTRICTION = "sell_restriction" # Cannot sell at all
HIGH_SELL_TAX = "high_sell_tax" # Excessive sell fee (>15%)
BUY_TAX = "buy_tax" # Excessive buy fee (>10%)
BLACKLIST = "blacklist" # Owner can block addresses
MAX_WALLET = "max_wallet" # Max holding limit
MAX_TX = "max_tx_amount" # Max transaction amount
COOLDOWN = "cooldown" # Trading cooldown timer
PROXY_UPGRADE = "proxy_upgrade" # Upgradable contract risk
OWNER_MINT = "owner_mint" # Owner can mint unlimited tokens
OWNER_BURN = "owner_burn" # Owner can burn from any address
LIQUIDITY_DRAIN = "liquidity_drain" # LP can be removed by owner
HIDDEN_MINT = "hidden_mint" # Hidden mint function
SELFDESTRUCT = "selfdestruct" # Contract can be destroyed
TRADING_PAUSE = "trading_pause" # Owner can pause trading
FAKE_BURN = "fake_burn" # Burns to address with known private key
RUG_PULL = "rug_pull" # Multiple combined scam indicators
UNKNOWN = "unknown"
@dataclass
class SecurityFinding:
"""A single security finding detected during analysis."""
finding_type: HoneypotType
severity: FindingSeverity
description: str
detail: str = ""
code_reference: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"type": self.finding_type.value,
"severity": self.severity.value,
"description": self.description,
"detail": self.detail,
"code_reference": self.code_reference,
}
@dataclass
class TokenInfo:
"""Basic token information gathered from on-chain and off-chain sources."""
address: str
chain: str
name: str = ""
symbol: str = ""
decimals: int = 18
total_supply: int = 0
holder_count: int = 0
creator_address: str = ""
creation_tx: str = ""
creation_timestamp: str = ""
is_verified: bool = False
source_code_url: str = ""
# Market data
price_usd: float = 0.0
liquidity_usd: float = 0.0
volume_24h_usd: float = 0.0
market_cap_usd: float = 0.0
# Ownership
owner_address: str = ""
ownership_renounced: bool = False
is_proxy: bool = False
implementation_address: str = ""
# Chain data sources used
chain_data_sources: list[str] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"address": self.address,
"chain": self.chain,
"name": self.name,
"symbol": self.symbol,
"decimals": self.decimals,
"total_supply": self.total_supply,
"holder_count": self.holder_count,
"creator": self.creator_address,
"age": self.creation_timestamp,
"verified": self.is_verified,
"price_usd": self.price_usd,
"liquidity_usd": self.liquidity_usd,
"volume_24h": self.volume_24h_usd,
"market_cap": self.market_cap_usd,
"owner": self.owner_address,
"ownership_renounced": self.ownership_renounced,
"is_proxy": self.is_proxy,
"implementation": self.implementation_address,
"chain_data_sources": self.chain_data_sources,
}
@dataclass
class HoneypotAnalysisResult:
"""Complete analysis result for a token."""
token: TokenInfo
# Findings
findings: list[SecurityFinding] = field(default_factory=list)
# Risk scoring
risk_score: int = 0 # 0-100
risk_label: str = "safe" # safe, low, medium, high, critical
# Detailed breakdowns
buy_tax_pct: float = 0.0
sell_tax_pct: float = 0.0
max_tx_pct: float = 0.0 # Max tx as % of supply
max_wallet_pct: float = 0.0 # Max wallet as % of supply
liquidity_locked_pct: float = 0.0 # What % of LP is locked
liquidity_lock_duration_days: int = 0
# Context
top_holders: list[dict[str, Any]] = field(default_factory=list)
suspicious_patterns: list[str] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
# Metadata
chain_data_sources: list[str] = field(default_factory=list)
scan_timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
scan_duration_ms: int = 0
def has_critical_findings(self) -> bool:
"""Check if any critical severity findings exist."""
return any(f.severity == FindingSeverity.CRITICAL for f in self.findings)
def has_high_findings(self) -> bool:
"""Check if any high severity findings exist."""
return any(f.severity == FindingSeverity.HIGH for f in self.findings)
def findings_by_severity(self, severity: FindingSeverity) -> list[SecurityFinding]:
"""Filter findings by severity level."""
return [f for f in self.findings if f.severity == severity]
def summary(self) -> str:
"""Human-readable one-line summary."""
critical = len(self.findings_by_severity(FindingSeverity.CRITICAL))
high = len(self.findings_by_severity(FindingSeverity.HIGH))
medium = len(self.findings_by_severity(FindingSeverity.MEDIUM))
return (
f"[{self.risk_label.upper()}] {self.token.symbol or '?'} "
f"({self.token.address[:10]}...) | "
f"Risk: {self.risk_score}/100 | "
f"Found: {critical}C/{high}H/{medium}M | "
f"Buy tax: {self.buy_tax_pct:.1f}% | "
f"Sell tax: {self.sell_tax_pct:.1f}% | "
f"LP locked: {self.liquidity_locked_pct:.0f}%"
)
def report(self, fmt: str = "text") -> str:
"""Generate a detailed report in text or JSON format."""
if fmt == "json":
return json.dumps(self.to_dict(), indent=2, default=str)
lines = []
lines.append("=" * 60)
lines.append(f"HONEYPOT ANALYSIS REPORT - {self.token.symbol or 'Unknown Token'}")
lines.append("=" * 60)
lines.append(f"Token: {self.token.address}")
lines.append(f"Chain: {self.token.chain}")
lines.append(f"Risk: {self.risk_score}/100 ({self.risk_label})")
lines.append(f"Price: ${self.token.price_usd:,.8f}")
lines.append(f"Liquidity: ${self.token.liquidity_usd:,.2f}")
lines.append(f"MCap: ${self.token.market_cap_usd:,.2f}")
lines.append("")
if self.buy_tax_pct > 0 or self.sell_tax_pct > 0:
lines.append("── Fee Structure ──")
lines.append(f" Buy tax: {self.buy_tax_pct:.1f}%")
lines.append(f" Sell tax: {self.sell_tax_pct:.1f}%")
lines.append("")
lines.append("── Findings ──")
if not self.findings:
lines.append(" ✅ No suspicious findings detected.")
else:
severity_emoji = {
FindingSeverity.CRITICAL: "🔴",
FindingSeverity.HIGH: "🟠",
FindingSeverity.MEDIUM: "🟡",
FindingSeverity.LOW: "🔵",
FindingSeverity.INFO: "",
}
for finding in self.findings:
emoji = severity_emoji.get(finding.severity, "")
lines.append(f" {emoji} [{finding.severity.value.upper()}] {finding.description}")
if finding.detail:
lines.append(f" {finding.detail}")
lines.append("")
if self.top_holders:
lines.append("── Top Holders ──")
for i, h in enumerate(self.top_holders[:5], 1):
pct = h.get("percentage", 0)
addr = h.get("address", "?")[:12]
lines.append(f" {i}. {addr}... - {pct:.1f}%")
lines.append("")
if self.warnings:
lines.append("── Warnings ──")
for w in self.warnings:
lines.append(f"{w}")
lines.append("")
lines.append(f"Scan completed: {self.scan_timestamp}")
return "\n".join(lines)
def to_dict(self) -> dict[str, Any]:
return {
"token": self.token.to_dict(),
"risk_score": self.risk_score,
"risk_label": self.risk_label,
"findings": [f.to_dict() for f in self.findings],
"buy_tax_pct": self.buy_tax_pct,
"sell_tax_pct": self.sell_tax_pct,
"liquidity_locked_pct": self.liquidity_locked_pct,
"liquidity_lock_duration_days": self.liquidity_lock_duration_days,
"top_holders": self.top_holders,
"warnings": self.warnings,
"chain_data_sources": self.chain_data_sources,
"scan_timestamp": self.scan_timestamp,
}
# ═══════════════════════════════════════════════════════════════════
# Constants - Known attack signatures and pattern databases
# ═══════════════════════════════════════════════════════════════════
# Known malicious function selectors (4-byte signature)
KNOWN_MALICIOUS_SELECTORS: dict[str, tuple[str, HoneypotType, str]] = {
# Blacklist functions
"0x24b6d0c4": (
"blacklist(address)",
HoneypotType.BLACKLIST,
"Owner can blacklist addresses from trading",
),
"0x9b3b0a8d": (
"addBlackList(address)",
HoneypotType.BLACKLIST,
"Owner can blacklist addresses",
),
"0x3d0de014": ("removeBlackList(address)", HoneypotType.BLACKLIST, ""),
"0x1f641f72": ("isBlackListed(address)", HoneypotType.BLACKLIST, "Blacklist lookup function"),
# Fee/cooldown manipulation
"0x8da7ad2a": (
"setCooldownEnabled(bool)",
HoneypotType.COOLDOWN,
"Owner can enable/disable trading cooldown",
),
"0xdd0655fc": (
"setTradingCooldown(uint256)",
HoneypotType.COOLDOWN,
"Owner can set trading cooldown duration",
),
"0x4261f4d8": (
"setMaxTxPercent(uint256)",
HoneypotType.MAX_TX,
"Owner can change max transaction amount",
),
"0x4a3b3a27": (
"setMaxWalletPercent(uint256)",
HoneypotType.MAX_WALLET,
"Owner can change max wallet holding",
),
"0x8f0f4b3a": ("setBuyFee(uint256)", HoneypotType.BUY_TAX, "Owner can change buy fee"),
"0xf0b9cf38": ("setSellFee(uint256)", HoneypotType.HIGH_SELL_TAX, "Owner can change sell fee"),
# Trading controls
"0x8456cb59": ("pause()", HoneypotType.TRADING_PAUSE, "Owner can pause all trading"),
"0x3f4ba83a": ("unpause()", HoneypotType.TRADING_PAUSE, "Owner can unpause trading"),
"0xbfb231d2": (
"setPaused(bool)",
HoneypotType.TRADING_PAUSE,
"Owner can pause/unpause trading",
),
# Mint/burn abuse
"0x40c10f19": (
"mint(address,uint256)",
HoneypotType.OWNER_MINT,
"Owner can mint tokens to any address",
),
"0x9dc29fac": (
"burn(address,uint256)",
HoneypotType.OWNER_BURN,
"Owner can burn tokens from any address",
),
"0x79cc6790": (
"burnFrom(address,uint256)",
HoneypotType.OWNER_BURN,
"Owner can burn from any address",
),
"0x42966c68": ("burn(uint256)", HoneypotType.UNKNOWN, "Token burn function"),
"0x40c10f18": ("mintTo(address,uint256)", HoneypotType.OWNER_MINT, ""),
# Liquidity manipulation
"0x2e1a7d4d": (
"withdraw(uint256)",
HoneypotType.LIQUIDITY_DRAIN,
"Owner can withdraw ETH/LP from contract",
),
"0xe1f21c67": ("removeLiquidity(uint256,uint256)", HoneypotType.LIQUIDITY_DRAIN, ""),
"0x02751cec": ("skim(address)", HoneypotType.UNKNOWN, ""),
"0xbc25cf77": ("sync()", HoneypotType.UNKNOWN, "Sync reserves - can manipulate price"),
# Selfdestruct
"0x41c0e1b5": (
"selfdestruct()",
HoneypotType.SELFDESTRUCT,
"Contract can selfdestruct - permanent loss",
),
"0x00f714ce": ("kill()", HoneypotType.SELFDESTRUCT, "Kill function - can destroy contract"),
# Proxy
"0x3659cfe6": (
"upgradeTo(address)",
HoneypotType.PROXY_UPGRADE,
"Proxy upgrade to new implementation",
),
"0xf2fde38b": (
"renounceOwnership()",
HoneypotType.UNKNOWN,
"Ownership renouncement function",
),
}
# Function selectors that are benign (common in standard ERC20)
BENIGN_SELECTORS: set[str] = {
"0x18160ddd", # totalSupply()
"0x70a08231", # balanceOf(address)
"0x313ce567", # decimals()
"0x06fdde03", # name()
"0x95d89b41", # symbol()
"0xdd62ed3e", # allowance(address,address)
"0xa9059cbb", # transfer(address,uint256)
"0x23b872dd", # transferFrom(address,address,uint256)
"0x095ea7b3", # approve(address,uint256)
"0x54fd4d50", # version()
"0x01ffc9a7", # supportsInterface(bytes4)
}
# EVM chains with known RPC endpoints
EVM_CHAINS: dict[str, dict[str, Any]] = {
"ethereum": {
"rpc": "https://eth.llamarpc.com",
"chain_id": 1,
"explorer": "https://api.etherscan.io/api",
},
"bsc": {
"rpc": "https://bsc-dataseed.binance.org",
"chain_id": 56,
"explorer": "https://api.bscscan.com/api",
},
"polygon": {
"rpc": "https://polygon-rpc.com",
"chain_id": 137,
"explorer": "https://api.polygonscan.com/api",
},
"arbitrum": {
"rpc": "https://arb1.arbitrum.io/rpc",
"chain_id": 42161,
"explorer": "https://api.arbiscan.io/api",
},
"optimism": {
"rpc": "https://mainnet.optimism.io",
"chain_id": 10,
"explorer": "https://api-optimistic.etherscan.io/api",
},
"base": {
"rpc": "https://mainnet.base.org",
"chain_id": 8453,
"explorer": "https://api.basescan.org/api",
},
"avalanche": {
"rpc": "https://api.avax.network/ext/bc/C/rpc",
"chain_id": 43114,
"explorer": "https://api.snowtrace.io/api",
},
}
# Honeypot threshold definitions
HONEYPOT_THRESHOLDS = {
"critical_sell_tax": 25.0, # If sell tax >= 25%, critical
"high_sell_tax": 15.0, # If sell tax >= 15%, high
"medium_sell_tax": 10.0, # If sell tax >= 10%, medium
"critical_buy_tax": 20.0, # If buy tax >= 20%, critical
"high_buy_tax": 10.0, # If buy tax >= 10%, high
"critical_lp_unlocked": 100.0, # 100% unlocked LP = critical
"high_lp_unlocked": 75.0, # >75% unlocked = high
"critical_supply_concentration": 90.0, # Single holder >90% = critical
"high_supply_concentration": 50.0, # Single holder >50% = high
"low_liquidity_threshold": 1000, # <$1000 liquidity
"tiny_liquidity_threshold": 100, # <$100 liquidity
}
# ═══════════════════════════════════════════════════════════════════
# HTTP Helper - Async fetch with retry and fallback
# ═══════════════════════════════════════════════════════════════════
async def fetch_with_fallback(
urls: list[str],
headers: dict[str, str] | None = None,
timeout: int = 15,
json_mode: bool = True,
) -> tuple[Any, str]:
"""Try URLs in order, return first successful response."""
import httpx
if headers is None:
headers = {"User-Agent": "RMI-Honeypot/1.0"}
async with httpx.AsyncClient(timeout=timeout) as client:
for url in urls:
try:
resp = await client.get(url, headers=headers)
if resp.status_code == 200:
if json_mode:
return resp.json(), url
return resp.text, url
logger.debug(f"HTTP {resp.status_code} from {url}")
except Exception as e:
logger.debug(f"Failed {url}: {e}")
continue
return {}, ""
# ═══════════════════════════════════════════════════════════════════
# Main Detector Class
# ═══════════════════════════════════════════════════════════════════
class HoneypotDetector:
"""Advanced honeypot and malicious token contract detector.
Analyzes token contracts across multiple EVM chains for signs of
honeypot mechanisms, rug-pull patterns, and malicious functionality.
Uses free/public data sources with layered fallback.
"""
def __init__(
self,
api_keys: dict[str, str] | None = None,
enable_deep_scan: bool = False,
):
self.api_keys = api_keys or {}
self.enable_deep_scan = enable_deep_scan
self._explorer_keys: dict[str, str] = self._load_explorer_keys()
def _load_explorer_keys(self) -> dict[str, str]:
"""Load explorer API keys from environment."""
return {
"ethereum": os.getenv("ETHERSCAN_KEY", ""),
"bsc": os.getenv("BSCSCAN_KEY", ""),
"polygon": os.getenv("POLYGONSCAN_KEY", ""),
"arbitrum": os.getenv("ARBISCAN_KEY", ""),
}
# ── Public API ─────────────────────────────────────────────────
async def analyze_token(
self,
address: str,
chain: str = "ethereum",
deep_scan: bool = False,
) -> HoneypotAnalysisResult:
"""Analyze a token contract for honeypot/malicious patterns.
Args:
address: Token contract address (0x... for EVM)
chain: Blockchain name (ethereum, bsc, polygon, etc.)
deep_scan: Enable deeper on-chain analysis (slower, more thorough)
Returns:
HoneypotAnalysisResult with all findings and risk score
"""
import time
start = time.monotonic()
address = address.strip().lower()
# Validate address
if not re.match(r"^0x[a-fA-F0-9]{40}$", address):
return HoneypotAnalysisResult(
token=TokenInfo(address=address, chain=chain),
risk_score=0,
risk_label="invalid",
warnings=[f"Invalid EVM address format: {address}"],
)
# Gather basic token info
token_info = await self._gather_token_info(address, chain)
# Perform analysis
result = HoneypotAnalysisResult(
token=token_info,
)
# Step 1: Check fee structure (buy/sell taxes)
await self._analyze_fees(address, chain, result)
# Step 2: Check ownership and upgrade risk
await self._analyze_ownership(address, chain, result)
# Step 3: Check contract functions for malicious selectors
await self._analyze_contract_functions(address, chain, result)
# Step 4: Check liquidity status
await self._analyze_liquidity(address, chain, result)
# Step 5: Check holder distribution
await self._analyze_holders(address, chain, result)
# Step 6: Market data and trading analysis
await self._analyze_market_data(address, chain, result)
# Step 7: Deep scan (if enabled) - on-chain simulation
if deep_scan or self.enable_deep_scan:
await self._deep_scan(address, chain, result)
# Compute final risk score
result.risk_score = self._compute_risk_score(result)
result.risk_label = self._risk_score_to_label(result.risk_score)
result.scan_duration_ms = int((time.monotonic() - start) * 1000)
return result
# ── Analysis Steps ────────────────────────────────────────────
async def _gather_token_info(self, address: str, chain: str) -> TokenInfo:
"""Gather basic token information from chain data and APIs."""
info = TokenInfo(address=address, chain=chain)
chain_info = EVM_CHAINS.get(chain, {})
# Try DexScreener for market data
try:
data, _src = await fetch_with_fallback(
[
f"https://api.dexscreener.com/latest/dex/tokens/{address}",
]
)
if data and data.get("pairs"):
pair = data["pairs"][0]
info.name = pair.get("baseToken", {}).get("name", "")
info.symbol = pair.get("baseToken", {}).get("symbol", "")
info.price_usd = float(pair.get("priceUsd", 0) or 0)
info.liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
info.volume_24h_usd = float(pair.get("volume", {}).get("h24", 0) or 0)
info.market_cap_usd = float(pair.get("fdv", 0) or 0)
info.chain_data_sources.append("dexscreener")
# Get pair address for liquidity checks
pair_addr = pair.get("pairAddress", "")
if pair_addr:
info.chain_data_sources.append(f"pair:{pair_addr}")
except Exception as e:
logger.debug(f"DexScreener failed: {e}")
# Try block explorer for contract verification
explorer_url = chain_info.get("explorer", "")
explorer_key = self._explorer_keys.get(chain, "")
if explorer_url:
try:
url = f"{explorer_url}?module=contract&action=getsourcecode&address={address}"
if explorer_key:
url += f"&apikey={explorer_key}"
data, _ = await fetch_with_fallback([url])
if data and data.get("status") == "1":
source = data["result"][0]
info.is_verified = source.get("SourceCode", "") != ""
info.creator_address = source.get("ContractCreator", "")
abi_str = source.get("ABI", "")
if abi_str and abi_str != "Contract source code not verified":
info.is_verified = True
info.chain_data_sources.append(f"explorer:{chain}")
except Exception as e:
logger.debug(f"Explorer failed: {e}")
return info
async def _analyze_fees(self, address: str, chain: str, result: HoneypotAnalysisResult) -> None:
"""Analyze token fee structure - buy/sell taxes."""
# Check via DexScreener for known fee tokens
try:
data, _ = await fetch_with_fallback(
[
f"https://api.dexscreener.com/latest/dex/tokens/{address}",
]
)
if data and data.get("pairs"):
for pair in data["pairs"]:
buy_tx = pair.get("txns", {}).get("h24", {}).get("buys", 0) or 0
sell_tx = pair.get("txns", {}).get("h24", {}).get("sells", 0) or 0
# If buy volume is high but sell volume is near zero,
# that's a honeypot indicator
if buy_tx > 10 and sell_tx == 0:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.SELL_RESTRICTION,
severity=FindingSeverity.CRITICAL,
description=f"Cannot sell tokens - 0 sells in 24h vs {buy_tx} buys",
detail=f"Found on {pair.get('dexId', '?')} - {pair.get('pairAddress', '')[:12]}...",
)
)
elif buy_tx > 50 and sell_tx < buy_tx * 0.05:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.HIGH_SELL_TAX,
severity=FindingSeverity.HIGH,
description="Extremely low sell volume - possible sell restriction",
detail=f"Buys: {buy_tx}, Sells: {sell_tx} (ratio: {sell_tx / max(buy_tx, 1):.2%})",
)
)
except Exception as e:
logger.debug(f"Fee analysis via DexScreener failed: {e}")
# Check via DexScreener for liquidity/trading patterns
try:
data, _ = await fetch_with_fallback(
[f"https://api.dexscreener.com/latest/dex/tokens/{address}"]
)
if data and data.get("pairs"):
pair = data["pairs"][0]
price_change = pair.get("priceChange", {}).get("h24", 0)
volume = float(pair.get("volume", {}).get("h24", 0) or 0)
liquidity = float(pair.get("liquidity", {}).get("usd", 0) or 0)
# Suspicious: massive price change with zero volume
if abs(price_change) > 90 and volume < 100:
result.warnings.append(
f"Extreme price change ({price_change:+.1f}%) with negligible volume"
)
# Suspicious: very high volume-to-liquidity ratio (wash trading)
if liquidity > 0 and volume > liquidity * 20:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.RUG_PULL,
severity=FindingSeverity.MEDIUM,
description=f"Unusually high volume/liquidity ratio ({volume / liquidity:.0f}x)",
detail="Possible wash trading or manipulation",
)
)
except Exception:
pass
async def _analyze_ownership(
self, address: str, chain: str, result: HoneypotAnalysisResult
) -> None:
"""Analyze ownership patterns - proxy, renouncement, admin keys."""
chain_info = EVM_CHAINS.get(chain, {})
rpc_url = chain_info.get("rpc", "")
if not rpc_url:
return
import httpx
try:
async with httpx.AsyncClient(timeout=10) as client:
# Check for EIP-1967 proxy implementation slot
impl_storage_slot = (
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
)
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": [address, impl_storage_slot, "latest"],
}
try:
resp = await client.post(rpc_url, json=payload)
if resp.status_code == 200:
impl_data = resp.json().get("result", "")
if impl_data and impl_data != "0x" + "0" * 64:
result.token.is_proxy = True
result.token.implementation_address = "0x" + impl_data[-40:]
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.PROXY_UPGRADE,
severity=FindingSeverity.HIGH,
description="Upgradable proxy contract detected",
detail=f"Implementation at {result.token.implementation_address}",
code_reference="EIP-1967 proxy storage slot",
)
)
except Exception:
pass
# Check for EIP-1967 admin slot
admin_slot = "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"
try:
payload["params"] = [address, admin_slot, "latest"]
resp = await client.post(rpc_url, json=payload)
if resp.status_code == 200:
admin_data = resp.json().get("result", "")
if admin_data and admin_data != "0x" + "0" * 64:
admin_addr = "0x" + admin_data[-40:]
result.token.owner_address = admin_addr
except Exception:
pass
except Exception as e:
logger.debug(f"Ownership analysis failed: {e}")
async def _analyze_contract_functions(
self, address: str, chain: str, result: HoneypotAnalysisResult
) -> None:
"""Analyze contract bytecode for malicious function selectors."""
chain_info = EVM_CHAINS.get(chain, {})
rpc_url = chain_info.get("rpc", "")
if not rpc_url:
return
import httpx
try:
async with httpx.AsyncClient(timeout=15) as client:
# Get contract bytecode
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getCode",
"params": [address, "latest"],
}
resp = await client.post(rpc_url, json=payload)
if resp.status_code != 200:
return
bytecode = resp.json().get("result", "")
if not bytecode or bytecode == "0x":
result.warnings.append("No contract bytecode found - not a contract")
return
# Remove '0x' prefix
bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode
# Extract all 4-byte selectors from bytecode
selectors_found: set[str] = set()
for i in range(0, len(bytecode) - 7, 2):
# Look for PUSH4 (0x63) followed by 4 bytes = function selector
if bytecode[i : i + 2] == "63":
selector = "0x" + bytecode[i + 2 : i + 10]
if len(selector) == 10: # 0x + 8 hex chars
selectors_found.add(selector)
# Also look for PUSH32 + keccak (DUP1 + PUSH4 + EQ + PUSH + JUMPI pattern)
if bytecode[i : i + 2] == "7f":
potential = "0x" + bytecode[i + 2 : i + 10]
if len(potential) == 10:
selectors_found.add(potential)
# Also detect via DEXScreener if available (ABI-based analysis)
try:
dex_url = f"https://api.dexscreener.com/latest/dex/tokens/{address}"
dex_data, _ = await fetch_with_fallback([dex_url])
if dex_data and dex_data.get("pairs"):
result.chain_data_sources.append("dexscreener_functions")
except Exception:
pass
# Check each found selector against known malicious patterns
for selector in selectors_found:
if selector in BENIGN_SELECTORS:
continue
if selector in KNOWN_MALICIOUS_SELECTORS:
sig, htype, desc = KNOWN_MALICIOUS_SELECTORS[selector]
# Skip benign ownership renouncement
if selector == "0xf2fde38b" and htype == HoneypotType.UNKNOWN:
continue
severity = FindingSeverity.MEDIUM
if htype in (
HoneypotType.SELFDESTRUCT,
HoneypotType.LIQUIDITY_DRAIN,
):
severity = FindingSeverity.CRITICAL
elif htype in (
HoneypotType.OWNER_MINT,
HoneypotType.PROXY_UPGRADE,
HoneypotType.BLACKLIST,
HoneypotType.TRADING_PAUSE,
):
severity = FindingSeverity.HIGH
result.findings.append(
SecurityFinding(
finding_type=htype,
severity=severity,
description=desc or f"Malicious function pattern: {sig}",
detail=f"Function: {sig}",
code_reference=f"Selector {selector} in bytecode",
)
)
# Check for selfdestruct opcode in bytecode
if "ff" in bytecode: # SELFDESTRUCT opcode
# Verify it's actual SELFDESTRUCT, not coincidental bytes
selfdestruct_patterns = [
"ff" in bytecode[i : i + 2] for i in range(0, len(bytecode) - 1)
]
if any(selfdestruct_patterns):
already_found = any(
f.finding_type == HoneypotType.SELFDESTRUCT for f in result.findings
)
if not already_found:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.SELFDESTRUCT,
severity=FindingSeverity.CRITICAL,
description="SELFDESTRUCT opcode found in contract bytecode",
detail="Contract can be permanently destroyed - all funds lost",
code_reference="SELFDESTRUCT (0xff) in bytecode",
)
)
# Check for delegatecall (proxy-like)
if "f4" in bytecode and not result.token.is_proxy:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.PROXY_UPGRADE,
severity=FindingSeverity.MEDIUM,
description="DELEGATECALL used - possible proxy or delegate pattern",
detail="Contract uses delegatecall pattern for execution forwarding",
code_reference="DELEGATECALL (0xf4) in bytecode",
)
)
except Exception as e:
logger.debug(f"Bytecode analysis failed: {e}")
async def _analyze_liquidity(
self, address: str, chain: str, result: HoneypotAnalysisResult
) -> None:
"""Analyze liquidity status - locked vs unlocked LP."""
# Check via DexScreener
try:
data, _ = await fetch_with_fallback(
[
f"https://api.dexscreener.com/latest/dex/tokens/{address}",
]
)
if data and data.get("pairs"):
for pair in data["pairs"]:
liquidity = float(pair.get("liquidity", {}).get("usd", 0) or 0)
pair_addr = pair.get("pairAddress", "")
# Very low liquidity check
if liquidity < HONEYPOT_THRESHOLDS["low_liquidity_threshold"]:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.RUG_PULL,
severity=(
FindingSeverity.CRITICAL
if liquidity < HONEYPOT_THRESHOLDS["tiny_liquidity_threshold"]
else FindingSeverity.HIGH
),
description=f"Extremely low liquidity: ${liquidity:.2f}",
detail=f"Pool: {pair.get('dexId', '?')} - {pair_addr[:12]}...",
)
)
# Check pair age
pair_created = pair.get("pairCreatedAt", 0)
if pair_created and liquidity > 1000:
age_hours = (datetime.now(UTC).timestamp() - pair_created / 1000) / 3600
if age_hours < 1:
result.warnings.append(
f"Pool is less than 1 hour old ({age_hours:.1f}h) - very high risk"
)
elif age_hours < 24:
result.warnings.append(
f"Pool is less than 24 hours old ({age_hours:.1f}h)"
)
except Exception as e:
logger.debug(f"Liquidity analysis failed: {e}")
# Try to find liquidity lock info from known lock contracts
# (Unicrypt, TeamFinance, Mudra, DXLock, etc.)
try:
# Check if LP tokens are burned or locked
# Look for zero-address burn of LP tokens
data, _ = await fetch_with_fallback(
[
f"https://api.dexscreener.com/latest/dex/tokens/{address}",
]
)
if data and data.get("pairs"):
for pair in data["pairs"]:
pair_addr = pair.get("pairAddress", "")
if pair_addr:
# Check LP holder via RPC (deeper scan)
chain_info = EVM_CHAINS.get(chain, {})
rpc_url = chain_info.get("rpc", "")
if rpc_url:
import httpx
async with httpx.AsyncClient(timeout=8) as client:
# Check if LP was burned (sent to 0x0...dEaD)
dead_addr = "0x000000000000000000000000000000000000dead"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getBalance",
"params": [dead_addr, "latest"],
}
try:
resp = await client.post(rpc_url, json=payload)
if resp.status_code == 200:
balance_hex = resp.json().get("result", "0x0")
balance = int(balance_hex, 16) if balance_hex else 0
if balance > 0:
result.liquidity_locked_pct = 100.0
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.UNKNOWN,
severity=FindingSeverity.INFO,
description="LP tokens appear to be burned (sent to dead address)",
detail="Liquidity cannot be removed if LP is burned",
)
)
except Exception:
pass
except Exception:
pass
async def _analyze_holders(
self, address: str, chain: str, result: HoneypotAnalysisResult
) -> None:
"""Analyze holder distribution for concentration risks."""
# Use DexScreener for basic holder info
try:
data, _ = await fetch_with_fallback(
[
f"https://api.dexscreener.com/latest/dex/tokens/{address}",
]
)
if data and data.get("pairs"):
# DexScreener doesn't provide holders directly,
# but we can infer from tx counts
result.chain_data_sources.append("dexscreener_holders")
except Exception:
pass
async def _analyze_market_data(
self, address: str, chain: str, result: HoneypotAnalysisResult
) -> None:
"""Analyze market data for manipulation signs."""
try:
data, _ = await fetch_with_fallback(
[
f"https://api.dexscreener.com/latest/dex/tokens/{address}",
]
)
if data and data.get("pairs"):
for pair in data["pairs"]:
price_change_h1 = pair.get("priceChange", {}).get("h1", 0) or 0
price_change_h24 = pair.get("priceChange", {}).get("h24", 0) or 0
volume_h24 = float(pair.get("volume", {}).get("h24", 0) or 0)
liquidity = float(pair.get("liquidity", {}).get("usd", 0) or 0)
# Detect pump and dump pattern
if price_change_h1 > 50 and liquidity < 50000:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.RUG_PULL,
severity=FindingSeverity.HIGH,
description=f"Suspicious pump pattern - +{price_change_h1:.0f}% in 1h with low liquidity",
detail="Rapid price increase with low liquidity is a classic rug-pull setup",
)
)
# Detect suspicious: >500% in 24h with tiny liquidity
if price_change_h24 > 500 and liquidity < 50000:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.RUG_PULL,
severity=FindingSeverity.CRITICAL,
description=f"Extreme price movement (+{price_change_h24:.0f}%) with low liquidity",
detail="Classic honeypot pattern - pump price to attract buyers, then block sells",
)
)
# Volume anomaly: huge volume on tiny pool
if liquidity > 0 and volume_h24 > liquidity * 50:
result.findings.append(
SecurityFinding(
finding_type=HoneypotType.RUG_PULL,
severity=FindingSeverity.MEDIUM,
description=f"Volume ({volume_h24:.0f}) is {volume_h24 / max(liquidity, 1):.0f}x liquidity - possible wash trading",
)
)
result.chain_data_sources.append("dexscreener_market")
except Exception:
pass
async def _deep_scan(self, address: str, chain: str, result: HoneypotAnalysisResult) -> None:
"""Deep scan - additional on-chain analysis for thorough detection.
Simulates a buy and sell to test actual honeypot behavior.
Requires RPC access to the chain.
"""
chain_info = EVM_CHAINS.get(chain, {})
rpc_url = chain_info.get("rpc", "")
if not rpc_url:
return
# In deep scan mode, attempt to:
# 1. Get recent transactions involving this token
# 2. Look for failed sell transactions (indicates honeypot)
# 3. Check more obscure malicious patterns
logger.debug(f"Deep scan enabled for {address[:10]} on {chain}")
# Check for recent transactions via RPC
import httpx
try:
async with httpx.AsyncClient(timeout=15) as client:
# Get latest block to check recent activity
block_payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_blockNumber",
"params": [],
}
resp = await client.post(rpc_url, json=block_payload)
if resp.status_code == 200:
latest_hex = resp.json().get("result", "0x0")
latest_block = int(latest_hex, 16)
result.chain_data_sources.append(f"rpc_block:{latest_block}")
except Exception:
pass
# ── Risk Scoring ─────────────────────────────────────────────
def _compute_risk_score(self, result: HoneypotAnalysisResult) -> int:
"""Compute aggregate risk score from all findings (0-100)."""
score = 0
for finding in result.findings:
if finding.severity == FindingSeverity.CRITICAL:
score += 35
elif finding.severity == FindingSeverity.HIGH:
score += 20
elif finding.severity == FindingSeverity.MEDIUM:
score += 10
elif finding.severity == FindingSeverity.LOW:
score += 3
# Cap at 100
score = min(score, 100)
# Minimum score even with findings
if score > 0 and score < 15:
score = 15
return score
def _risk_score_to_label(self, score: int) -> str:
"""Convert numeric risk score to human-readable label."""
if score >= 70:
return "critical"
if score >= 40:
return "high"
if score >= 20:
return "medium"
if score >= 5:
return "low"
return "safe"
# ═══════════════════════════════════════════════════════════════════
# CLI Entry Point
# ═══════════════════════════════════════════════════════════════════
async def main_async() -> None:
"""CLI entry point with argument parsing."""
import argparse
parser = argparse.ArgumentParser(
description="Smart Contract Honeypot & Malicious Token Detector",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 smart_contract_honeypot_detector.py 0x...
python3 smart_contract_honeypot_detector.py 0x... --chain bsc
python3 smart_contract_honeypot_detector.py 0x... --deep-scan
python3 smart_contract_honeypot_detector.py 0x... --format json
python3 smart_contract_honeypot_detector.py 0x... --chain polygon --format json --deep-scan
""",
)
parser.add_argument("address", type=str, help="Token contract address (0x...)")
parser.add_argument(
"--chain",
type=str,
default="ethereum",
choices=list(EVM_CHAINS.keys()),
help="Blockchain (default: ethereum)",
)
parser.add_argument(
"--deep-scan",
action="store_true",
help="Enable deeper on-chain analysis (slower)",
)
parser.add_argument(
"--format",
type=str,
choices=["text", "json"],
default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
detector = HoneypotDetector()
result = await detector.analyze_token(
address=args.address,
chain=args.chain,
deep_scan=args.deep_scan,
)
if args.format == "json":
print(result.report(fmt="json"))
else:
print(result.report())
def main() -> None:
"""Synchronous CLI entry point."""
asyncio.run(main_async())
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
main()