rmi-backend/app/mev_protection.py

721 lines
27 KiB
Python

"""
MEV Shield Analysis — Proactive Transaction Protection
========================================================
Assesses any pending or planned transaction for MEV extraction vulnerability
BEFORE it's sent. Identifies sandwich attack risk, frontrunning exposure,
and backrunning vulnerability. Provides actionable protection strategies.
The tool analyzes:
- MEMPOOL: Pending transaction patterns in the block's proximity
- SLIPPAGE: How much slippage tolerance invites MEV extraction
- GAS_PRICE: Whether gas bidding makes the tx a target
- PAIR_LIQUIDITY: How thin liquidity enables sandwich attacks
- TOKEN_PAIR: Historical MEV activity on this specific pair
- TIMING: Peak vs off-peak MEV activity periods
TOOL : mev_protection
TIER : premium
PRICE : $0.08 (80000 atoms)
TRIAL : 1 free check
Data Sources (all free):
- Local MEV database (historical sandwich/frontrun patterns)
- DexScreener — pool liquidity, pair metadata
- Chain RPC (public) — pending tx pool analysis
- Mempool.space (BTC) and Etherscan pending (EVM)
- Historical MEV data from mev_sandwich_detector
"""
import asyncio
import logging
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any
import httpx
__all__ = [
"MevFactor",
"MevRiskLevel",
"MevShieldResult",
"analyze_transaction_risk",
"mev_shield_analysis",
]
logger = logging.getLogger(__name__)
# ── Constants ────────────────────────────────────────────────────
MEV_API_TIMEOUT = 10
DEFAULT_SLIPPAGE_BPS = 100 # 1% = 100 bps
DEFAULT_GAS_PRICE_GWEI = 10
HIGH_RISK_SLIPPAGE_BPS = 300 # 3%+
HIGH_RISK_GAS_PRICE_GWEI = 50
THIN_LIQUIDITY_USD = 50_000
MODERATE_LIQUIDITY_USD = 250_000
FREE_RPCS = {
"ethereum": "https://eth.llamarpc.com",
"bsc": "https://bsc-dataseed.binance.org",
"base": "https://mainnet.base.org",
"arbitrum": "https://arb1.arbitrum.io/rpc",
"polygon": "https://polygon-rpc.com",
"optimism": "https://mainnet.optimism.io",
"avalanche": "https://api.avax.network/ext/bc/C/rpc",
}
FALLBACK_RPCS = {
"ethereum": ["https://rpc.ankr.com/eth", "https://ethereum-rpc.publicnode.com"],
"bsc": ["https://rpc.ankr.com/bsc", "https://bsc-rpc.publicnode.com"],
"base": ["https://base-rpc.publicnode.com", "https://rpc.base.llama.com"],
}
# ── Risk Levels ──────────────────────────────────────────────────
class MevRiskLevel(StrEnum):
LOW = "low"
MODERATE = "moderate"
HIGH = "high"
CRITICAL = "critical"
# ── Data Models ──────────────────────────────────────────────────
@dataclass
class MevFactor:
name: str
score: float # 0.0 (safe) to 1.0 (very vulnerable)
finding: str
detail: str
suggestion: str | None = None
@dataclass
class MevShieldResult:
risk_level: MevRiskLevel
overall_score: float # 0.0 (safe) to 1.0 (critical)
factors: list[MevFactor]
protection_strategies: list[str]
estimated_loss_bps: int # Expected loss in basis points if targeted
timestamp: str
tx_params: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"risk_level": self.risk_level.value,
"overall_score": round(self.overall_score, 2),
"estimated_loss_bps": self.estimated_loss_bps,
"factors": [
{
"name": f.name,
"score": round(f.score, 2),
"finding": f.finding,
"detail": f.detail,
"suggestion": f.suggestion,
}
for f in self.factors
],
"protection_strategies": self.protection_strategies,
"timestamp": self.timestamp,
"tx_params": self.tx_params,
}
# ── MEV Shield Analysis Engine ────────────────────────────────────
async def _fetch_free_rpc(chain: str, method: str, params: list[Any] | None = None) -> dict[str, Any] | None:
"""Call an RPC with fallback URLs."""
urls: list[str] = [FREE_RPCS.get(chain, "")]
if chain in FALLBACK_RPCS:
urls.extend(FALLBACK_RPCS[chain])
for url in urls:
if not url:
continue
try:
async with httpx.AsyncClient(timeout=MEV_API_TIMEOUT) as client:
resp = await client.post(
url,
json={
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params or [],
},
headers={"Content-Type": "application/json"},
)
if resp.status_code == 200:
data: dict[str, Any] = resp.json()
if "error" not in data:
return data
except Exception as e:
logger.debug(f"RPC {url} failed: {e}")
continue
return None
async def _check_mempool_risk(chain: str, pair_address: str | None = None) -> MevFactor:
"""
Check pending transaction pool for signs of MEV activity.
Uses pending tx count and gas price distribution as proxies.
Note: pair_address is reserved for future pair-specific mempool analysis.
Currently performs chain-level mempool congestion assessment only.
"""
if chain not in FREE_RPCS:
return MevFactor(
name="mempool_activity",
score=0.3,
finding=f"Unsupported chain: {chain}",
detail=f"No RPC endpoints configured for {chain} — mempool analysis unavailable",
suggestion="Supported chains: " + ", ".join(sorted(FREE_RPCS.keys())),
)
score = 0.1
findings = []
details = []
# Get pending transaction count
result = await _fetch_free_rpc(chain, "txpool_status")
if result and "result" in result:
try:
pending = int(result["result"].get("pending", "0x0"), 16)
if pending > 500:
score = 0.8
findings.append("Congested mempool")
details.append(f"{pending} pending transactions — high MEV competition zone")
elif pending > 200:
score = 0.5
findings.append("Moderate mempool activity")
details.append(f"{pending} pending transactions — MEV possible")
else:
score = 0.2
findings.append("Low mempool activity")
details.append(f"{pending} pending transactions — lower MEV likelihood")
except (ValueError, KeyError):
pass
# Check gas prices in pending pool
result2 = await _fetch_free_rpc(chain, "txpool_content")
if result2 and "result" in result2:
try:
pending_txs = result2["result"].get("pending", {})
gas_prices = []
for _nonce_key, txs in pending_txs.items():
if isinstance(txs, dict):
for _, tx in txs.items():
if isinstance(tx, dict) and "gasPrice" in tx:
gas_prices.append(int(tx["gasPrice"], 16))
if gas_prices:
avg_gas = sum(gas_prices) / len(gas_prices) / 1e9 # convert to gwei
if avg_gas > 100:
score = max(score, 0.75)
findings.append("High gas price environment")
details.append(f"Avg gas: {avg_gas:.1f} gwei — competitive bidding indicates MEV activity")
elif avg_gas > 30:
score = max(score, 0.45)
findings.append("Elevated gas prices")
details.append(f"Avg gas: {avg_gas:.1f} gwei — some MEV extraction likely")
except (ValueError, KeyError):
pass
finding = "; ".join(findings) if findings else "Mempool looks quiet"
detail = "; ".join(details) if details else "No unusual mempool activity detected"
suggestions = {
0.8: "Use private mempool (Flashbots, MEV Blocker) to avoid frontrunning",
0.5: "Consider using a MEV-protected RPC endpoint",
0.2: "Standard transaction should be safe — no special protection needed",
}
# Pick closest suggestion
closest_key = min(suggestions.keys(), key=lambda k: abs(k - score))
suggestion = suggestions[closest_key]
return MevFactor(
name="mempool_activity",
score=round(score, 2),
finding=finding,
detail=detail,
suggestion=suggestion,
)
async def _check_slippage_risk(slippage_bps: int) -> MevFactor:
"""Assess how slippage tolerance exposes the transaction to MEV."""
if slippage_bps >= HIGH_RISK_SLIPPAGE_BPS:
score = 0.9
finding = "High slippage tolerance"
detail = f"{slippage_bps / 100:.1f}% slippage — easily exploited by sandwich bots"
suggestion = "Reduce slippage to 0.5-1.0% (50-100 bps) to minimize attack surface"
elif slippage_bps >= DEFAULT_SLIPPAGE_BPS:
score = 0.5
finding = "Moderate slippage tolerance"
detail = f"{slippage_bps / 100:.1f}% slippage — exploitable in thin liquidity pools"
suggestion = "Lower slippage to 0.5% for tighter protection"
else:
score = 0.15
finding = "Low slippage tolerance"
detail = f"{slippage_bps / 100:.1f}% slippage — tight, harder to sandwich"
suggestion = "Current slippage setting looks safe"
return MevFactor(
name="slippage_tolerance",
score=round(score, 2),
finding=finding,
detail=detail,
suggestion=suggestion,
)
async def _check_gas_price_risk(gas_price_gwei: float) -> MevFactor:
"""Assess if gas price makes the transaction a target."""
if gas_price_gwei >= HIGH_RISK_GAS_PRICE_GWEI:
score = 0.7
finding = "Premium gas price"
detail = f"{gas_price_gwei:.1f} gwei — high bid flags tx as urgent/value to bots"
suggestion = "Use Flashbots to submit privately while still getting fast inclusion"
elif gas_price_gwei >= 20:
score = 0.4
finding = "Above-average gas price"
detail = f"{gas_price_gwei:.1f} gwei — may attract some MEV attention"
suggestion = "Consider MEV Blocker or a private RPC for extra safety"
else:
score = 0.1
finding = "Normal gas price"
detail = f"{gas_price_gwei:.1f} gwei — unlikely to attract MEV extractors"
suggestion = "Standard gas pricing — no special MEV concern"
return MevFactor(
name="gas_price",
score=round(score, 2),
finding=finding,
detail=detail,
suggestion=suggestion,
)
async def _check_pool_liquidity(pair_address: str | None, chain: str) -> MevFactor:
"""Check if pool liquidity is thin enough to enable sandwich attacks."""
if not pair_address:
return MevFactor(
name="pool_liquidity",
score=0.3,
finding="Unknown pool",
detail="No pair address provided — cannot assess liquidity depth",
suggestion="Provide pair address for accurate MEV risk assessment",
)
try:
async with httpx.AsyncClient(timeout=MEV_API_TIMEOUT) as client:
resp = await client.get(f"https://api.dexscreener.com/latest/dex/pairs/{chain}/{pair_address}")
if resp.status_code == 200:
data = resp.json()
pairs = data.get("pairs", [])
if pairs:
pair = pairs[0]
liquidity_usd = float(pair.get("liquidity", {}).get("usd", 0) or 0)
if liquidity_usd < THIN_LIQUIDITY_USD:
score = 0.85
finding = "Thin liquidity pool"
detail = (
f"${liquidity_usd:,.0f} liquidity — highly vulnerable to sandwich/liquidity manipulation"
)
suggestion = "Avoid trading in sub-$50K liquidity pools, or use small orders split across time"
elif liquidity_usd < MODERATE_LIQUIDITY_USD:
score = 0.5
finding = "Moderate liquidity"
detail = f"${liquidity_usd:,.0f} liquidity — sandwich possible for orders >${liquidity_usd * 0.01:,.0f}"
suggestion = (
"Keep individual trades under 1% of pool liquidity to minimize slippage and MEV risk"
)
else:
score = 0.15
finding = "Deep liquidity pool"
detail = f"${liquidity_usd:,.0f} liquidity — sandwich attacks expensive and unlikely"
suggestion = "Low MEV risk due to deep liquidity"
return MevFactor(
name="pool_liquidity",
score=round(score, 2),
finding=finding,
detail=detail,
suggestion=suggestion,
)
except Exception as e:
logger.debug(f"DexScreener lookup failed: {e}")
return MevFactor(
name="pool_liquidity",
score=0.3,
finding="Liquidity lookup failed",
detail="Could not fetch pool data — conservative risk estimate applied",
suggestion="Check manually on DexScreener or retry later",
)
async def _check_historical_mev(chain: str, token_address: str | None = None) -> MevFactor:
"""
Check if this chain/token pair has a history of MEV attacks.
Uses the existing mev_sandwich_detector module if available.
"""
score = 0.2
try:
# Try to import the MEV detector for historical data
from app.mev_sandwich_detector import MEVSandwichDetector
detector = MEVSandwichDetector()
stats = await detector.scan()
if stats and hasattr(stats, "sandwiches"):
recent_attacks = len(stats.sandwiches)
if recent_attacks > 20:
score = 0.75
finding = "Active MEV chain"
detail = f"{recent_attacks} MEV attacks in last 24h — high activity chain"
suggestion = "Always use MEV protection on this chain (Flashbots, MEV Blocker)"
elif recent_attacks > 5:
score = 0.45
finding = "Moderate MEV activity"
detail = f"{recent_attacks} MEV attacks in last 24h — some risk"
suggestion = "Consider MEV protection for high-value transactions"
else:
score = 0.15
finding = "Low MEV activity"
detail = f"{recent_attacks} MEV attacks in last 24h — chain relatively safe"
suggestion = "Standard transactions should be safe"
return MevFactor(
name="historical_mev",
score=round(score, 2),
finding=finding,
detail=detail,
suggestion=suggestion,
)
except (ImportError, AttributeError):
pass
except Exception as e:
logger.debug(f"Historical MEV check failed: {e}")
# Fallback: use chain-level heuristics
chain_risk = {
"ethereum": 0.6,
"bsc": 0.5,
"base": 0.4,
"arbitrum": 0.35,
"polygon": 0.3,
}
score = chain_risk.get(chain, 0.25)
return MevFactor(
name="historical_mev",
score=round(score, 2),
finding=f"Default risk for {chain}",
detail=f"{chain} has {'high' if score > 0.5 else 'moderate' if score > 0.3 else 'low'} historical MEV activity",
suggestion="Check chain-specific MEV statistics for accurate assessment",
)
async def _check_timing_risk() -> MevFactor:
"""Assess if current time is a high-MEV period."""
now = datetime.now(UTC)
hour = now.hour
# Peak MEV hours tend to be during US market hours (14-22 UTC)
# and during high-volatility news events
is_weekend = now.weekday() >= 5
if is_weekend:
score = 0.3
finding = "Weekend trading"
detail = "Lower overall MEV activity on weekends — fewer bots competing"
suggestion = "Good time for lower-risk transactions"
elif 14 <= hour <= 22:
score = 0.6
finding = "Peak MEV hours"
detail = "US market hours — highest MEV bot competition"
suggestion = (
"Consider transacting outside peak hours (before 14:00 UTC) for lower MEV risk, or use private mempool"
)
elif 6 <= hour <= 13:
score = 0.35
finding = "Off-peak hours"
detail = "Lower bot activity during Asian/European hours"
suggestion = "Moderate MEV risk — standard precautions recommended"
else:
score = 0.2
finding = "Low activity period"
detail = "Nighttime hours — minimal bot activity expected"
suggestion = "Low MEV risk window"
return MevFactor(
name="timing",
score=round(score, 2),
finding=finding,
detail=detail,
suggestion=suggestion,
)
def _compute_protection_strategies(risk_level: MevRiskLevel, factors: list[MevFactor], chain: str) -> list[str]:
"""Generate actionable protection strategies based on risk profile."""
strategies = []
# Check high-risk factors and suggest specific protections
high_factors = [f for f in factors if f.score >= 0.6]
if risk_level in (MevRiskLevel.CRITICAL, MevRiskLevel.HIGH):
strategies.append(
"🚨 Use Flashbots Protect (https://flashbots.net/) for private transaction submission — "
"guarantees your tx won't be frontrun"
)
strategies.append(
"🔒 Use MEV Blocker (https://mevblocker.io/) — sends tx to private mempool with backrunning protection"
)
if any(f.name == "pool_liquidity" for f in high_factors):
strategies.append("📊 Split large trades into smaller chunks to reduce slippage and sandwich exposure")
if any(f.name == "slippage_tolerance" for f in high_factors):
strategies.append(
"⚡ Set slippage to 0.5% or lower — this is the single most effective MEV reduction tactic"
)
elif risk_level == MevRiskLevel.MODERATE:
strategies.append("🛡️ Consider MEV Blocker for moderate-value transactions — free to use")
strategies.append("⏰ Time your transaction during off-peak hours (before 14:00 UTC) for lower MEV competition")
else:
strategies.append("✅ Standard transaction appears safe — no special MEV protection needed")
strategies.append("💡 For high-value transactions (>$10K), still consider Flashbots as a precaution")
# Chain-specific
if chain == "ethereum":
strategies.append(
"🔷 Ethereum has the most mature MEV protection — Flashbots, MEV Blocker, and CoW Swap all available"
)
elif chain == "bsc":
strategies.append(
"🟡 BSC has fewer MEV protection options — Flashbots not available. "
"Use low slippage and consider Poly Network for cross-chain routing"
)
return strategies
def _compute_estimated_loss(risk_level: MevRiskLevel, factors: list[MevFactor]) -> int:
"""Estimate expected loss in basis points if MEV'd."""
base_loss = {
MevRiskLevel.LOW: 10,
MevRiskLevel.MODERATE: 50,
MevRiskLevel.HIGH: 150,
MevRiskLevel.CRITICAL: 300,
}
loss = base_loss[risk_level]
# Adjust based on specific factors
for f in factors:
if f.name == "pool_liquidity" and f.score > 0.7:
loss = int(loss * 1.5) # Thin liquidity amplifies losses
if f.name == "slippage_tolerance" and f.score > 0.7:
loss = int(loss * 1.3) # High slippage enables bigger sandwich
if f.name == "mempool_activity" and f.score > 0.7:
loss = int(loss * 1.2) # Competitive mempool = better bots
return min(loss, 500) # Cap at 5% max estimated loss
async def _resolve_chain_id(chain: str) -> str:
"""Normalize chain name to internal format."""
chain_map = {
"eth": "ethereum",
"ether": "ethereum",
"bsc": "bsc",
"bnb": "bsc",
"base": "base",
"arb": "arbitrum",
"arbitrum": "arbitrum",
"polygon": "polygon",
"matic": "polygon",
"op": "optimism",
"optimism": "optimism",
"avax": "avalanche",
"avalanche": "avalanche",
}
return chain_map.get(chain.lower(), chain.lower())
async def analyze_transaction_risk(
chain: str = "ethereum",
pair_address: str | None = None,
token_address: str | None = None,
slippage_bps: int | None = None,
gas_price_gwei: float | None = None,
) -> MevShieldResult:
"""
Full MEV Shield Analysis — assess transaction MEV vulnerability.
Args:
chain: Blockchain name (ethereum, bsc, base, arbitrum, polygon, optimism, avalanche)
pair_address: DEX pair address for liquidity check (DexScreener format)
token_address: Token address for historical MEV lookup
slippage_bps: Slippage tolerance in basis points (default: 100 = 1%)
gas_price_gwei: Gas price in gwei (default: auto-detect from RPC)
Returns:
MevShieldResult with risk assessment, factor breakdown, and protection strategies
"""
chain = await _resolve_chain_id(chain)
if chain not in FREE_RPCS:
raise ValueError(f"Unsupported chain: '{chain}'. Supported chains: {', '.join(sorted(FREE_RPCS.keys()))}")
slippage_bps = slippage_bps or DEFAULT_SLIPPAGE_BPS
if gas_price_gwei is None:
# Auto-detect gas price
result = await _fetch_free_rpc(chain, "eth_gasPrice")
if result and "result" in result:
try:
gas_price_gwei = int(result["result"], 16) / 1e9
except (ValueError, KeyError):
gas_price_gwei = DEFAULT_GAS_PRICE_GWEI
else:
gas_price_gwei = DEFAULT_GAS_PRICE_GWEI
# Run all risk checks in parallel
(
mempool_factor,
slippage_factor,
gas_factor,
liquidity_factor,
historical_factor,
timing_factor,
) = await asyncio.gather(
_check_mempool_risk(chain, pair_address),
_check_slippage_risk(slippage_bps),
_check_gas_price_risk(gas_price_gwei),
_check_pool_liquidity(pair_address, chain),
_check_historical_mev(chain, token_address),
_check_timing_risk(),
)
factors = [
mempool_factor,
slippage_factor,
gas_factor,
liquidity_factor,
historical_factor,
timing_factor,
]
# Compute overall score (weighted average)
weights = {
"mempool_activity": 0.20,
"slippage_tolerance": 0.25,
"gas_price": 0.15,
"pool_liquidity": 0.20,
"historical_mev": 0.10,
"timing": 0.10,
}
overall_score = sum(f.score * weights.get(f.name, 0.15) for f in factors) / sum(weights.values())
# Determine risk level
if overall_score >= 0.7:
risk_level = MevRiskLevel.CRITICAL
elif overall_score >= 0.5:
risk_level = MevRiskLevel.HIGH
elif overall_score >= 0.3:
risk_level = MevRiskLevel.MODERATE
else:
risk_level = MevRiskLevel.LOW
protection_strategies = _compute_protection_strategies(risk_level, factors, chain)
estimated_loss_bps = _compute_estimated_loss(risk_level, factors)
return MevShieldResult(
risk_level=risk_level,
overall_score=round(overall_score, 2),
factors=factors,
protection_strategies=protection_strategies,
estimated_loss_bps=estimated_loss_bps,
timestamp=datetime.now(UTC).isoformat(),
tx_params={
"chain": chain,
"slippage_bps": slippage_bps,
"gas_price_gwei": round(gas_price_gwei, 1),
"pair_address": pair_address,
"token_address": token_address,
},
)
# ── CLI Entry Point ──────────────────────────────────────────────
def mev_shield_analysis(
chain: str = "ethereum",
pair_address: str | None = None,
token_address: str | None = None,
slippage_bps: int | None = None,
gas_price_gwei: float | None = None,
) -> dict[str, Any]:
"""Synchronous wrapper for MEV Shield Analysis. Use in non-async contexts."""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
# Already in an event loop — create a new one in a new thread
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
future = pool.submit(
asyncio.run,
analyze_transaction_risk(
chain=chain,
pair_address=pair_address,
token_address=token_address,
slippage_bps=slippage_bps,
gas_price_gwei=gas_price_gwei,
),
)
return future.result().to_dict()
except RuntimeError:
pass
return asyncio.run(
analyze_transaction_risk(
chain=chain,
pair_address=pair_address,
token_address=token_address,
slippage_bps=slippage_bps,
gas_price_gwei=gas_price_gwei,
)
).to_dict()
# ── Main (standalone test) ───────────────────────────────────────
async def main() -> None:
"""Run a sample MEV Shield analysis for demonstration."""
print("🛡️ MEV Shield Analysis — Demo Run")
print("=" * 60)
result = await analyze_transaction_risk(
chain="ethereum",
pair_address="0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640", # USDC/WETH
slippage_bps=300, # 3% — high risk
)
print(f"\nRisk Level: {result.risk_level.value.upper()}")
print(f"Overall Score: {result.overall_score:.2f} / 1.00")
print(f"Estimated Loss if MEV'd: {result.estimated_loss_bps} bps ({result.estimated_loss_bps / 100:.1f}%)")
print()
print("Factor Breakdown:")
for f in result.factors:
bar = "" * int(f.score * 20) + "" * (20 - int(f.score * 20))
print(f" [{bar}] {f.name:25s} {f.score:.2f}{f.finding}")
print("\nProtection Strategies:")
for s in result.protection_strategies:
print(f"{s}")
print(f"\nTimestamp: {result.timestamp}")
if __name__ == "__main__":
asyncio.run(main())