rmi-backend/tests/unit/test_smart_contract_honeypot_detector.py

376 lines
12 KiB
Python

"""
Tests for smart_contract_honeypot_detector.py
"""
import json
import sys
from pathlib import Path
# Add backend to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from app.smart_contract_honeypot_detector import (
HONEYPOT_THRESHOLDS,
FindingSeverity,
HoneypotAnalysisResult,
HoneypotDetector,
HoneypotType,
SecurityFinding,
TokenInfo,
)
def test_security_finding_creation() -> None:
"""Test SecurityFinding dataclass creation and serialization."""
finding = SecurityFinding(
finding_type=HoneypotType.SELL_RESTRICTION,
severity=FindingSeverity.CRITICAL,
description="Cannot sell tokens",
detail="0 sells vs 100 buys in 24h",
code_reference="dexscreener_tx_analysis",
)
d = finding.to_dict()
assert d["type"] == "sell_restriction"
assert d["severity"] == "critical"
assert d["description"] == "Cannot sell tokens"
assert d["detail"] == "0 sells vs 100 buys in 24h"
def test_security_finding_defaults() -> None:
"""Test SecurityFinding with default values."""
finding = SecurityFinding(
finding_type=HoneypotType.HIGH_SELL_TAX,
severity=FindingSeverity.HIGH,
description="High sell tax",
)
assert finding.detail == ""
assert finding.code_reference == ""
def test_finding_severity_enum() -> None:
"""Test FindingSeverity enum values."""
assert FindingSeverity.CRITICAL.value == "critical"
assert FindingSeverity.HIGH.value == "high"
assert FindingSeverity.MEDIUM.value == "medium"
assert FindingSeverity.LOW.value == "low"
assert FindingSeverity.INFO.value == "info"
def test_honeypot_type_enum() -> None:
"""Test HoneypotType enum values."""
assert HoneypotType.SELL_RESTRICTION.value == "sell_restriction"
assert HoneypotType.BLACKLIST.value == "blacklist"
assert HoneypotType.PROXY_UPGRADE.value == "proxy_upgrade"
assert HoneypotType.SELFDESTRUCT.value == "selfdestruct"
assert HoneypotType.LIQUIDITY_DRAIN.value == "liquidity_drain"
assert HoneypotType.OWNER_MINT.value == "owner_mint"
def test_token_info_creation() -> None:
"""Test TokenInfo creation and serialization."""
info = TokenInfo(
address="0x1234567890abcdef1234567890abcdef12345678",
chain="ethereum",
name="Test Token",
symbol="TEST",
decimals=18,
price_usd=0.01,
liquidity_usd=50000,
chain_data_sources=["dexscreener", "explorer:ethereum"],
)
d = info.to_dict()
assert d["address"] == "0x1234567890abcdef1234567890abcdef12345678"
assert d["chain"] == "ethereum"
assert d["name"] == "Test Token"
assert d["symbol"] == "TEST"
assert d["price_usd"] == 0.01
assert d["liquidity_usd"] == 50000
assert len(d["chain_data_sources"]) == 2
def test_token_info_defaults() -> None:
"""Test TokenInfo default values."""
info = TokenInfo(address="0x0", chain="bsc")
assert info.name == ""
assert info.symbol == ""
assert info.decimals == 18
assert info.price_usd == 0.0
assert info.is_verified is False
assert info.ownership_renounced is False
assert info.is_proxy is False
assert info.chain_data_sources == []
def test_analysis_result_empty() -> None:
"""Test HoneypotAnalysisResult with no findings."""
token = TokenInfo(
address="0xabc",
chain="ethereum",
name="Safe Token",
symbol="SAFE",
)
result = HoneypotAnalysisResult(
token=token,
risk_score=0,
risk_label="safe",
)
assert not result.has_critical_findings()
assert not result.has_high_findings()
assert result.findings_by_severity(FindingSeverity.CRITICAL) == []
assert result.findings_by_severity(FindingSeverity.MEDIUM) == []
assert result.risk_score == 0
assert result.risk_label == "safe"
def test_analysis_result_with_findings() -> None:
"""Test HoneypotAnalysisResult with findings."""
token = TokenInfo(address="0xdef", chain="bsc")
result = HoneypotAnalysisResult(
token=token,
risk_score=65,
risk_label="high",
findings=[
SecurityFinding(
finding_type=HoneypotType.SELL_RESTRICTION,
severity=FindingSeverity.CRITICAL,
description="Cannot sell",
),
SecurityFinding(
finding_type=HoneypotType.OWNER_MINT,
severity=FindingSeverity.HIGH,
description="Owner can mint",
),
SecurityFinding(
finding_type=HoneypotType.COOLDOWN,
severity=FindingSeverity.MEDIUM,
description="Trading cooldown",
),
],
)
assert result.has_critical_findings()
assert result.has_high_findings()
assert len(result.findings_by_severity(FindingSeverity.CRITICAL)) == 1
assert len(result.findings_by_severity(FindingSeverity.HIGH)) == 1
assert len(result.findings_by_severity(FindingSeverity.MEDIUM)) == 1
assert len(result.findings_by_severity(FindingSeverity.LOW)) == 0
def test_analysis_result_summary() -> None:
"""Test result summary format."""
token = TokenInfo(
address="0xdead00000000000000000000000000000000beef",
chain="polygon",
symbol="SCAM",
)
result = HoneypotAnalysisResult(
token=token,
risk_score=80,
risk_label="critical",
findings=[
SecurityFinding(
finding_type=HoneypotType.SELL_RESTRICTION,
severity=FindingSeverity.CRITICAL,
description="Cannot sell",
),
],
buy_tax_pct=5.0,
sell_tax_pct=50.0,
liquidity_locked_pct=0.0,
)
summary = result.summary()
assert "CRITICAL" in summary
assert "SCAM" in summary
assert "80/100" in summary
assert "Buy tax: 5.0%" in summary
assert "Sell tax: 50.0%" in summary
assert "LP locked: 0%" in summary
def test_analysis_result_report_text() -> None:
"""Test text report format."""
token = TokenInfo(
address="0xabc123",
chain="ethereum",
symbol="TEST",
price_usd=0.05,
liquidity_usd=10000,
)
result = HoneypotAnalysisResult(
token=token,
risk_score=25,
risk_label="medium",
findings=[
SecurityFinding(
finding_type=HoneypotType.COOLDOWN,
severity=FindingSeverity.MEDIUM,
description="Trading cooldown enabled",
),
],
buy_tax_pct=3.0,
sell_tax_pct=12.0,
)
report = result.report(fmt="text")
assert "HONEYPOT ANALYSIS REPORT" in report
assert "TEST" in report
assert "medium" in report
assert "Buy tax:" in report
assert "Sell tax:" in report
assert "Trading cooldown" in report
def test_analysis_result_report_json() -> None:
"""Test JSON report format."""
token = TokenInfo(address="0x1111", chain="base", symbol="TEST")
result = HoneypotAnalysisResult(
token=token,
risk_score=50,
risk_label="high",
findings=[
SecurityFinding(
finding_type=HoneypotType.BLACKLIST,
severity=FindingSeverity.HIGH,
description="Blacklist function",
),
],
)
report = result.report(fmt="json")
parsed = json.loads(report)
assert parsed["risk_score"] == 50
assert parsed["risk_label"] == "high"
assert parsed["token"]["address"] == "0x1111"
assert len(parsed["findings"]) == 1
assert parsed["findings"][0]["type"] == "blacklist"
def test_analysis_result_to_dict() -> None:
"""Test result dict serialization includes all fields."""
token = TokenInfo(address="0xaaaa", chain="arbitrum")
result = HoneypotAnalysisResult(
token=token,
risk_score=100,
risk_label="critical",
warnings=["Test warning"],
buy_tax_pct=10.0,
sell_tax_pct=30.0,
liquidity_locked_pct=0.0,
)
d = result.to_dict()
assert d["risk_score"] == 100
assert d["risk_label"] == "critical"
assert "Test warning" in d["warnings"]
assert d["buy_tax_pct"] == 10.0
assert d["sell_tax_pct"] == 30.0
assert d["liquidity_locked_pct"] == 0.0
assert "scan_timestamp" in d
def test_invalid_address() -> None:
"""Test detector handles invalid addresses gracefully."""
import asyncio
detector = HoneypotDetector()
async def _test() -> None:
result = await detector.analyze_token("not-an-address", chain="ethereum")
assert result.risk_label == "invalid"
assert "Invalid EVM address" in result.warnings[0]
asyncio.run(_test())
def test_risk_score_minimum() -> None:
"""Test risk score has minimum floor."""
detector = HoneypotDetector()
# If score < 15 was set by the compute function, it gets bumped
assert detector._risk_score_to_label(3) == "safe" # Below floor of 5
assert detector._risk_score_to_label(15) == "low"
assert detector._risk_score_to_label(25) == "medium"
assert detector._risk_score_to_label(45) == "high"
assert detector._risk_score_to_label(75) == "critical"
def test_risk_score_thresholds() -> None:
"""Test risk score to label mapping."""
detector = HoneypotDetector()
assert detector._risk_score_to_label(0) == "safe"
assert detector._risk_score_to_label(4) == "safe"
assert detector._risk_score_to_label(5) == "low"
assert detector._risk_score_to_label(19) == "low"
assert detector._risk_score_to_label(20) == "medium"
assert detector._risk_score_to_label(39) == "medium"
assert detector._risk_score_to_label(40) == "high"
assert detector._risk_score_to_label(69) == "high"
assert detector._risk_score_to_label(70) == "critical"
assert detector._risk_score_to_label(100) == "critical"
def test_known_malicious_selectors() -> None:
"""Test that known malicious selectors are properly defined."""
from app.smart_contract_honeypot_detector import KNOWN_MALICIOUS_SELECTORS
assert "0x40c10f19" in KNOWN_MALICIOUS_SELECTORS # mint(address,uint256)
assert "0x24b6d0c4" in KNOWN_MALICIOUS_SELECTORS # blacklist
assert "0x41c0e1b5" in KNOWN_MALICIOUS_SELECTORS # selfdestruct
assert "0x3659cfe6" in KNOWN_MALICIOUS_SELECTORS # upgradeTo
assert "0x8456cb59" in KNOWN_MALICIOUS_SELECTORS # pause
assert "0x2e1a7d4d" in KNOWN_MALICIOUS_SELECTORS # withdraw
# Verify each has proper tuple structure
for selector, (sig, htype, desc) in KNOWN_MALICIOUS_SELECTORS.items():
assert selector.startswith("0x")
assert len(selector) == 10 # 0x + 8 hex chars
assert isinstance(sig, str)
assert isinstance(htype, HoneypotType)
assert isinstance(desc, str)
def test_benign_selectors() -> None:
"""Test benign selectors don't conflict with malicious ones."""
from app.smart_contract_honeypot_detector import (
BENIGN_SELECTORS,
KNOWN_MALICIOUS_SELECTORS,
)
# Benign and malicious should not overlap
overlap = set(BENIGN_SELECTORS) & set(KNOWN_MALICIOUS_SELECTORS.keys())
assert len(overlap) == 0, f"Overlap found: {overlap}"
def test_honeypot_thresholds() -> None:
"""Test honeypot threshold values are reasonable."""
assert HONEYPOT_THRESHOLDS["critical_sell_tax"] >= 20
assert HONEYPOT_THRESHOLDS["high_sell_tax"] >= 10
assert HONEYPOT_THRESHOLDS["critical_buy_tax"] >= 15
assert HONEYPOT_THRESHOLDS["high_buy_tax"] >= 8
assert HONEYPOT_THRESHOLDS["low_liquidity_threshold"] > 0
assert HONEYPOT_THRESHOLDS["tiny_liquidity_threshold"] > 0
def test_evm_chains() -> None:
"""Test EVM chain configuration is complete."""
from app.smart_contract_honeypot_detector import EVM_CHAINS
required_chains = {"ethereum", "bsc", "polygon", "arbitrum", "base"}
for chain in required_chains:
assert chain in EVM_CHAINS, f"Missing chain: {chain}"
assert "rpc" in EVM_CHAINS[chain]
assert "chain_id" in EVM_CHAINS[chain]
assert "explorer" in EVM_CHAINS[chain]
assert EVM_CHAINS[chain]["chain_id"] > 0
def test_honeypot_detector_init() -> None:
"""Test HoneypotDetector initialization."""
detector = HoneypotDetector()
assert detector.enable_deep_scan is False
assert detector.api_keys == {}
assert "ethereum" in detector._explorer_keys
detector2 = HoneypotDetector(enable_deep_scan=True)
assert detector2.enable_deep_scan is True
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])