rmi-backend/tests/unit/test_dex_pool_manipulation.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

409 lines
13 KiB
Python

"""
Tests for DEXPoolManipulationAnalyzer.
"""
import pytest
from app.dex_pool_manipulation_analyzer import (
DEXPoolManipulationAnalyzer,
RiskCategory,
RiskSignal,
format_risk_report,
is_valid_address,
)
@pytest.fixture
def analyzer():
return DEXPoolManipulationAnalyzer(chain="ethereum", dex="uniswap_v3")
@pytest.fixture
def sample_pool_metadata():
return {
"chain": "ethereum",
"dex": "uniswap_v3",
"version": "v3",
"token0": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"token1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"token0_symbol": "WETH",
"token1_symbol": "USDC",
"fee_tier": 500, # 0.05%
"tick_spacing": 10,
"total_liquidity_usd": 5000000.0,
"owner": "0x1234567890123456789012345678901234567890",
"created_at": 1700000000,
}
@pytest.fixture
def sample_positions():
return [
{
"owner": "0x1111...1111",
"tick_lower": -100,
"tick_upper": 100,
"liquidity": 500_000,
"usd_value": 1_000_000,
},
{
"owner": "0x2222...2222",
"tick_lower": -50,
"tick_upper": 50,
"liquidity": 300_000,
"usd_value": 600_000,
},
{
"owner": "0x3333...3333",
"tick_lower": -200,
"tick_upper": 200,
"liquidity": 100_000,
"usd_value": 200_000,
},
{
"owner": "0x4444...4444",
"tick_lower": -150,
"tick_upper": 150,
"liquidity": 50_000,
"usd_value": 100_000,
},
{
"owner": "0x5555...5555",
"tick_lower": -80,
"tick_upper": 80,
"liquidity": 30_000,
"usd_value": 60_000,
},
{
"owner": "0x6666...6666",
"tick_lower": -300,
"tick_upper": 300,
"liquidity": 20_000,
"usd_value": 40_000,
},
]
@pytest.fixture
def concentrated_positions():
"""Positions where top 1 owner controls most liquidity."""
return [
{
"owner": "0xBEEF...BEEF",
"tick_lower": -60,
"tick_upper": 60,
"liquidity": 900_000,
"usd_value": 1_800_000,
},
{
"owner": "0xBEEF...BEEF",
"tick_lower": -40,
"tick_upper": 40,
"liquidity": 50_000,
"usd_value": 100_000,
},
{
"owner": "0xCAFE...CAFE",
"tick_lower": -100,
"tick_upper": 100,
"liquidity": 30_000,
"usd_value": 60_000,
},
{
"owner": "0xDEAD...DEAD",
"tick_lower": -200,
"tick_upper": 200,
"liquidity": 20_000,
"usd_value": 40_000,
},
]
@pytest.fixture
def sandwich_swaps():
"""Simulated sandwich attack pattern."""
return [
{
"tx_hash": "0xaaa",
"block": 100,
"timestamp": 1000,
"amount_in": 5,
"amount_out": 4950,
"price_before": 1000.0,
"price_after": 1005.0,
},
{
"tx_hash": "0xbbb",
"block": 100,
"timestamp": 1001,
"amount_in": 50,
"amount_out": 49500,
"price_before": 1005.0,
"price_after": 1001.0,
},
{
"tx_hash": "0xccc",
"block": 100,
"timestamp": 1002,
"amount_in": 5,
"amount_out": 4980,
"price_before": 1001.0,
"price_after": 1000.5,
},
{
"tx_hash": "0xddd",
"block": 200,
"timestamp": 2000,
"amount_in": 10,
"amount_out": 10000,
"price_before": 1050.0,
"price_after": 1055.0,
},
{
"tx_hash": "0xeee",
"block": 200,
"timestamp": 2001,
"amount_in": 8,
"amount_out": 7950,
"price_before": 1055.0,
"price_after": 1051.0,
},
]
class TestAddressValidation:
def test_valid_evm_address(self):
assert is_valid_address("0x1234567890abcdef1234567890abcdef12345678")
def test_valid_solana_address(self):
assert is_valid_address("7e8qUqNYBg4QvfVj5H4GqYBKF9HbDQ4XQcBJnvcMqrZN")
def test_invalid_address(self):
assert not is_valid_address("not_an_address")
assert not is_valid_address("")
assert not is_valid_address("0xshort")
class TestPoolConfig:
def test_build_from_metadata(self, analyzer, sample_pool_metadata):
pool = analyzer._build_pool_config("0xpool...pool", sample_pool_metadata)
assert pool.address == "0xpool...pool"
assert pool.chain == "ethereum"
assert pool.dex == "uniswap_v3"
assert pool.fee_tier == 500
assert pool.total_liquidity_usd == 5_000_000.0
def test_defaults_for_empty_metadata(self, analyzer):
pool = analyzer._build_pool_config("0xpool...pool", {})
assert pool.chain == "ethereum"
assert pool.total_liquidity_usd == 0.0
class TestConcentrationAnalysis:
def test_no_positions(self, analyzer, sample_pool_metadata):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
signal, pct = analyzer._analyze_concentration([], pool)
assert signal is None
assert pct == 0.0
def test_well_distributed(self, analyzer, sample_pool_metadata, sample_positions):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
parsed = analyzer._parse_positions(sample_positions)
signal, _pct = analyzer._analyze_concentration(parsed, pool)
# Top 5: 500+300+100+50+30 = 980k out of 1000k = 98% - should flag
assert signal is not None
assert signal.category == RiskCategory.LIQUIDITY_CONCENTRATION
def test_concentrated_ownership(self, analyzer, sample_pool_metadata, concentrated_positions):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
parsed = analyzer._parse_positions(concentrated_positions)
signal, pct = analyzer._analyze_concentration(parsed, pool)
assert signal is not None
assert pct > 90 # Single owner controls >90%
class TestSandwichDetection:
def test_detect_sandwich(self, analyzer, sample_pool_metadata, sandwich_swaps):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
parsed = analyzer._parse_swaps(sandwich_swaps)
signal, profit = analyzer._analyze_sandwich_vulnerability(parsed, pool)
assert signal is not None
assert signal.category == RiskCategory.SANDWICH_VULNERABILITY
assert profit >= 0
def test_no_swaps_no_signal(self, analyzer, sample_pool_metadata):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
signal, profit = analyzer._analyze_sandwich_vulnerability([], pool)
assert signal is None
assert profit == 0.0
class TestPriceManipulation:
def test_high_impact_detected(self, analyzer, sample_pool_metadata):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
swaps = [
{
"tx_hash": "0xa",
"block": 1,
"timestamp": 1000,
"amount_in": 100,
"amount_out": 50,
"price_before": 100.0,
"price_after": 110.0,
},
{
"tx_hash": "0xb",
"block": 2,
"timestamp": 1001,
"amount_in": 50,
"amount_out": 20,
"price_before": 110.0,
"price_after": 115.0,
},
]
parsed = analyzer._parse_swaps(swaps)
signal = analyzer._analyze_price_manipulation(parsed, pool)
assert signal is not None
assert signal.category == RiskCategory.PRICE_MANIPULATION
def test_low_impact_no_signal(self, analyzer, sample_pool_metadata):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
swaps = [
{
"tx_hash": "0xa",
"block": 1,
"timestamp": 1000,
"amount_in": 1,
"amount_out": 999,
"price_before": 1000.0,
"price_after": 1000.1,
},
]
parsed = analyzer._parse_swaps(swaps)
signal = analyzer._analyze_price_manipulation(parsed, pool)
assert signal is None
class TestRiskScoring:
def test_no_signals_zero_score(self, analyzer):
score = analyzer._calculate_risk_score([])
assert score == 0.0
def test_high_severity_signals(self, analyzer):
signals = [
RiskSignal(
category=RiskCategory.LIQUIDITY_CONCENTRATION,
severity=0.8,
description="High concentration",
),
RiskSignal(
category=RiskCategory.SANDWICH_VULNERABILITY,
severity=0.8,
description="High sandwich risk",
),
RiskSignal(category=RiskCategory.FAKE_LIQUIDITY, severity=0.7, description="Fake liquidity"),
]
score = analyzer._calculate_risk_score(signals)
assert 0 < score <= 100
assert score > 30 # Should be significant
class TestPriceImpact:
def test_impact_increases_with_amount(self, analyzer):
impact_small = analyzer._simulate_price_impact(1, 1_000_000)
impact_large = analyzer._simulate_price_impact(100, 1_000_000)
assert impact_large > impact_small
assert impact_small < 1.0 # 1 ETH in $1M pool should be small
def test_impact_capped(self, analyzer):
impact = analyzer._simulate_price_impact(1_000_000, 1_000)
assert impact <= 99.99
def test_zero_liquidity(self, analyzer):
impact = analyzer._simulate_price_impact(1, 0)
assert impact > 100
class TestFullAnalysis:
@pytest.mark.asyncio
async def test_healthy_pool(self, analyzer, sample_pool_metadata, sample_positions):
swaps = [
{
"tx_hash": "0xa",
"block": 1,
"timestamp": 1000,
"amount_in": 0.1,
"amount_out": 100,
"price_before": 1000.0,
"price_after": 1000.01,
},
{
"tx_hash": "0xb",
"block": 2,
"timestamp": 1001,
"amount_in": 0.2,
"amount_out": 200,
"price_before": 1000.01,
"price_after": 1000.03,
},
]
report = await analyzer.analyze_pool(
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
recent_swaps=swaps,
positions=sample_positions,
pool_metadata=sample_pool_metadata,
)
assert report.risk_score >= 0
assert report.analysis_time_ms >= 0
assert len(report.signals) >= 0
assert report.pool.address is not None
@pytest.mark.asyncio
async def test_risky_pool(self, analyzer, concentrated_positions, sandwich_swaps):
meta = {
"chain": "ethereum",
"dex": "uniswap_v3",
"version": "v3",
"token0_symbol": "SHIT",
"token1_symbol": "USDC",
"fee_tier": 0,
"total_liquidity_usd": 5000.0,
"owner": "0xDEAD...DEAD",
"created_at": 1000,
}
report = await analyzer.analyze_pool(
"0xDEAD00000000000000000000000000000000BEEF",
recent_swaps=sandwich_swaps,
positions=concentrated_positions,
pool_metadata=meta,
)
assert report.risk_score > 25
assert report.price_impact_1eth > 0.01
assert len(report.recommendations) > 0
class TestReportFormat:
def test_format(self, analyzer, sample_pool_metadata):
pool = analyzer._build_pool_config("0xpool", sample_pool_metadata)
# Build minimal report
from app.dex_pool_manipulation_analyzer import PoolRiskReport, RiskCategory, RiskSignal
report_obj = PoolRiskReport(
pool=pool,
risk_score=45.5,
signals=[RiskSignal(RiskCategory.LIQUIDITY_CONCENTRATION, 0.5, "Test signal")],
price_impact_1eth=0.05,
price_impact_10eth=0.5,
price_impact_100eth=5.0,
top_5_concentration_pct=85.0,
liquidity_depth_1pct=100000.0,
sandwich_profit_estimate=0.01,
recommendations=["Test recommendation"],
analysis_time_ms=42,
)
result = format_risk_report(report_obj)
assert result["risk_score"] == 45.5
assert result["risk_level"] == "high"
assert len(result["signals"]) == 1
assert len(result["recommendations"]) == 1
assert len(result["metrics"]["price_impact"]) == 3