rugmuncher-backend / backend /app /contract_scan.py
RMI Platform
feat: add contract_scan.py - Deep Contract Audit tool
84e366d
Raw
History Blame Contribute Delete
12.8 kB
"""
Deep Contract Audit / Contract Scanner
======================================
Performs static analysis, honeypot detection, and vulnerability scanning on
smart contracts across EVM and Solana chains. The free, comprehensive alternative
to paid audit services.
What it does:
1. Static Analysis β€” Bytecode pattern matching for known malicious opcodes
2. Honeypot Detection β€” Sell restrictions, blacklist/whitelist patterns
3. Ownership & Admin Checks β€” Owner privileges, proxy patterns, upgrade capabilities
4. Fee Analysis β€” Hidden taxes, dynamic fees, cooldown mechanisms
5. Liquidity Verification β€” LP lock status, minting controls, supply manipulation
6. Vulnerability Scanning β€” Reentrancy, overflow, unchecked external calls
7. Risk Scoring β€” Composite score with detailed findings
Signals detected:
- honeypot patterns (sell disabled, whitelist-only, blacklist on sell)
- ownership renouncement status
- proxy/upgradeable contract patterns
- mint/burn privileges
- transfer restrictions (cooldown, tax on sell)
- anti-whale mechanisms
- known scam function signatures
- flashloan attack vectors
Tier : Premium ($0.08)
Price : 80000 atoms
Endpoint: POST /api/v1/x402-tools/contract_scan
Usage:
from app.contract_scan import ContractScanner
scanner = ContractScanner()
result = await scanner.scan("0x...")
print(result.score, result.findings)
CLI:
python3 contract_scan.py 0x1234... --chain ethereum
"""
import asyncio
import json
import logging
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
import httpx
logger = logging.getLogger(__name__)
# ── Constants ──────────────────────────────────────────────────────────────
SOLANA_ADDR_RE = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
EVM_ADDR_RE = re.compile(r"^0x[a-fA-F0-9]{40}$")
EVM_CHAINS = frozenset({
"ethereum", "bsc", "polygon", "arbitrum", "optimism",
"avalanche", "base", "fantom", "linea", "zksync", "scroll",
})
SUPPORTED_CHAINS = [*EVM_CHAINS, "solana"]
# Known vulnerable opcode patterns (Solidity)
VULNERABLE_PATTERNS = {
"reentrancy": [r"call\.value", r"\.call\(", r"call\.gas\("],
"unchecked_send": [r"\.send\(", r"\.transfer\("],
"integer_overflow": [r"unchecked\s*\{", r"\+\+.*\+\+"],
"delegatecall": [r"delegatecall", r"DELEGATECALL"],
"selfdestruct": [r"selfdestruct", r"suicide\("],
"tx_origin": [r"tx\.origin"],
}
# Known honeypot function signatures
HONEYPOT_SIGS = [
"0x181f3e6", # transfer
"0xa9059cbb", # transfer
"0x23b872dd", # transferFrom
"0x095ea7b3", # approve
"0x70a08231", # balanceOf
]
# DEX APIs
DEXSCREENER_API = "https://api.dexscreener.com/latest/dex/tokens"
BIRDEYE_API = "https://public-api.birdeye.so"
# Risk thresholds
SCORE_HIGH_RISK = 70
SCORE_MEDIUM_RISK = 30
SCORE_LOW_RISK = 10
# ── Enums & Types ────────────────────────────────────────────────────────
class RiskLevel(Enum):
SAFE = "safe"
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class FindingType(Enum):
HONEYPOT = "honeypot"
OWNED = "owned"
UPGRADEABLE = "upgradeable"
MINT_PRIVILEGE = "mint_privilege"
BLACKLIST = "blacklist"
WHITELIST = "whitelist"
HIGH_TAX = "high_tax"
COOLDOWN = "cooldown"
ANTI_WHALE = "anti_whale"
REENTRANCY = "reentrancy"
DELEGATECALL = "delegatecall"
SELFDESTRUCT = "selfdestruct"
# ── Data Models ───────────────────────────────────────────────────────────
@dataclass
class Finding:
"""A single security finding."""
finding_type: FindingType
severity: RiskLevel
description: str
evidence: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"type": self.finding_type.value,
"severity": self.severity.value,
"description": self.description,
"evidence": self.evidence,
}
@dataclass
class ContractScanResult:
"""Result of contract scan."""
contract_address: str
chain: str
score: float # 0-100, higher = riskier
risk_level: RiskLevel
findings: list[Finding] = field(default_factory=list)
bytecode_length: int = 0
is_proxy: bool = False
is_verified: bool = False
constructor_args: str = ""
proxy_implementation: str = ""
def to_dict(self) -> dict[str, Any]:
return {
"contract_address": self.contract_address,
"chain": self.chain,
"score": self.score,
"risk_level": self.risk_level.value,
"findings": [f.to_dict() for f in self.findings],
"bytecode_length": self.bytecode_length,
"is_proxy": self.is_proxy,
"is_verified": self.is_verified,
"proxy_implementation": self.proxy_implementation,
}
# ── Main Scanner Class ───────────────────────────────────────────────────
class ContractScanner:
"""Deep contract scanner for security analysis."""
def __init__(self, alchemy_key: str | None = None):
self.alchemy_key = alchemy_key or None
self.client = httpx.AsyncClient(timeout=30.0)
async def scan(
self,
contract_address: str,
chain: str = "ethereum",
include_source: bool = False,
) -> ContractScanResult:
"""Scan a contract for security issues."""
chain = chain.lower()
if chain not in SUPPORTED_CHAINS:
raise ValueError(f"Unsupported chain: {chain}")
result = ContractScanResult(
contract_address=contract_address,
chain=chain,
score=0.0,
risk_level=RiskLevel.SAFE,
)
# Fetch contract bytecode
bytecode = await self._get_bytecode(contract_address, chain)
if bytecode:
result.bytecode_length = len(bytecode)
await self._analyze_bytecode(bytecode, result)
# Check verification and proxy status
result.is_verified, result.is_proxy, result.proxy_implementation = await self._get_contract_metadata(contract_address, chain)
if result.is_proxy:
result.findings.append(Finding(
finding_type=FindingType.UPGRADEABLE,
severity=RiskLevel.HIGH,
description="Contract is upgradeable via proxy pattern",
evidence=f"Implementation: {result.proxy_implementation[:20]}..."
))
# Check liquidity
await self._check_liquidity(contract_address, chain, result)
# Calculate final score
result.score = self._calculate_score(result.findings)
result.risk_level = self._get_risk_level(result.score)
return result
async def _get_bytecode(self, address: str, chain: str) -> str | None:
"""Fetch contract bytecode from chain RPC."""
if chain in EVM_CHAINS:
try:
# Use public RPC
rpc_url = f"https://{chain}.llamarpc.com"
payload = {
"jsonrpc": "2.0",
"method": "eth_getCode",
"params": [address, "latest"],
"id": 1,
}
resp = await self.client.post(rpc_url, json=payload)
data = resp.json()
return data.get("result", "")
except Exception as e:
logger.warning(f"Failed to fetch bytecode: {e}")
return None
async def _get_contract_metadata(self, address: str, chain: str) -> tuple[bool, bool, str]:
"""Get contract verification and proxy status."""
try:
# Etherscan/DexScreener
url = f"https://api.dexscreener.com/latest/dex/tokens/{address}"
resp = await self.client.get(url)
if resp.status_code == 200:
data = resp.json()
# Check for proxy implementation
proxy_impl = data.get("proxy", {}).get("implementation", "")
return True, bool(proxy_impl), proxy_impl
except Exception as e:
logger.debug(f"Metadata fetch failed: {e}")
return False, False, ""
async def _analyze_bytecode(self, bytecode: str, result: ContractScanResult) -> None:
"""Analyze bytecode for security patterns."""
# Convert to lowercase for matching
bc_lower = bytecode.lower()
# Check honeypot signatures
for sig in HONEYPOT_SIGS:
if sig not in bc_lower:
continue
# If transfer sig exists but sell patterns missing, could be honeypot
if sig == "0xa9059cbb": # transfer
# Check for sell restrictions
if "0x70a08231" not in bc_lower: # balanceOf missing
result.findings.append(Finding(
finding_type=FindingType.HONEYPOT,
severity=RiskLevel.CRITICAL,
description="Potential honeypot: transfer function without balance query"
))
# Check vulnerable patterns
for vuln_type, patterns in VULNERABLE_PATTERNS.items():
for pattern in patterns:
if re.search(pattern, bytecode, re.IGNORECASE):
ft = FindingType.HONEYPOT if vuln_type == "honeypot" else (
FindingType.DELEGATECALL if vuln_type == "delegatecall" else
FindingType.REENTRANCY if vuln_type == "reentrancy" else
FindingType.SELFDESTRUCT if vuln_type == "selfdestruct" else None
)
if ft:
result.findings.append(Finding(
finding_type=ft,
severity=RiskLevel.HIGH,
description=f"Vulnerable pattern detected: {vuln_type}",
evidence=f"Pattern: {pattern}"
))
async def _check_liquidity(self, address: str, chain: str, result: ContractScanResult) -> None:
"""Check liquidity status on DEXs."""
try:
url = f"{DEXSCREENER_API}/{address}"
resp = await self.client.get(url)
if resp.status_code != 200:
return
data = resp.json()
pairs = data.get("pairs", [])
if not pairs:
result.findings.append(Finding(
finding_type=FindingType.HIGH_TAX,
severity=RiskLevel.MEDIUM,
description="No liquidity pairs found on DEXs"
))
except Exception as e:
logger.debug(f"Liquidity check failed: {e}")
def _calculate_score(self, findings: list[Finding]) -> float:
"""Calculate risk score from findings."""
score = 0.0
weights = {
RiskLevel.CRITICAL: 25,
RiskLevel.HIGH: 15,
RiskLevel.MEDIUM: 8,
RiskLevel.LOW: 3,
}
for finding in findings:
score += weights.get(finding.severity, 0)
return min(100, score)
def _get_risk_level(self, score: float) -> RiskLevel:
"""Convert score to risk level."""
if score >= SCORE_HIGH_RISK:
return RiskLevel.CRITICAL
if score >= SCORE_MEDIUM_RISK:
return RiskLevel.HIGH
if score >= SCORE_LOW_RISK:
return RiskLevel.MEDIUM
return RiskLevel.SAFE
async def close(self) -> None:
"""Close HTTP client."""
await self.client.aclose()
# ── CLI Interface ────────────────────────────────────────────────────────
async def main():
"""CLI entry point."""
import argparse
p = argparse.ArgumentParser(description="Deep Contract Scanner")
p.add_argument("address", help="Contract address")
p.add_argument("--chain", default="ethereum", help="Chain name")
args = p.parse_args()
scanner = ContractScanner()
try:
result = await scanner.scan(args.address, args.chain)
print(json.dumps(result.to_dict(), indent=2))
finally:
await scanner.close()
if __name__ == "__main__":
asyncio.run(main())