feat(bot): Anti-MEV protection on DEX swap commands
Checks token age, liquidity, volume ratio before generating swap links. Warns users about sandwich attack risk and recommends proper slippage based on risk level. Cache: 5-min in-memory for repeated queries on same token.
This commit is contained in:
parent
0cbd59fe18
commit
9c0964807f
1 changed files with 67 additions and 0 deletions
|
|
@ -356,3 +356,70 @@ async def cmd_holders(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|||
await update.message.reply_text(
|
||||
text, parse_mode=ParseMode.HTML, reply_markup=back_kb(), disable_web_page_preview=True
|
||||
)
|
||||
|
||||
|
||||
# ── Anti-MEV Protection ─────────────────────────────────────────────────
|
||||
|
||||
MEV_RISK_CACHE = {} # Simple in-memory cache
|
||||
|
||||
async def _check_mev_risk(token_address: str, chain: str) -> dict:
|
||||
"""Check MEV/sandwich risk for a token before generating swap link."""
|
||||
import time
|
||||
cache_key = f"{token_address}:{chain}"
|
||||
cached = MEV_RISK_CACHE.get(cache_key)
|
||||
if cached and time.time() - cached["ts"] < 300: # 5 min cache
|
||||
return cached
|
||||
|
||||
risk = {"risk": "unknown", "warnings": [], "recommend_slippage": 2.0}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
r = await client.get(f"{DEXSCREENER}/tokens/{token_address}")
|
||||
pairs = r.json().get("pairs", [])
|
||||
if pairs:
|
||||
p = pairs[0]
|
||||
age_h = float(p.get("pairCreatedAt", 0))
|
||||
age_days = (time.time() / 1000 - age_h / 1000) / 86400 if age_h else 999
|
||||
liquidity = float(p.get("liquidity", {}).get("usd", 0) or 0)
|
||||
volume = float(p.get("volume", {}).get("h24", 0) or 0)
|
||||
txns = int(p.get("txns", {}).get("h24", {}).get("buys", 0) or 0) + int(p.get("txns", {}).get("h24", {}).get("sells", 0) or 0)
|
||||
|
||||
if age_days < 1:
|
||||
risk["warnings"].append(f"🆕 Token is {age_days*24:.0f}h old — high sandwich risk")
|
||||
risk["risk"] = "critical"
|
||||
risk["recommend_slippage"] = 25.0
|
||||
elif age_days < 7:
|
||||
risk["warnings"].append(f"⚠️ Token only {age_days:.1f} days old — moderate risk")
|
||||
risk["risk"] = "high"
|
||||
risk["recommend_slippage"] = 15.0
|
||||
|
||||
if liquidity < 10000:
|
||||
risk["warnings"].append(f"💧 Low liquidity (${liquidity:,.0f}) — easily manipulated")
|
||||
if risk["risk"] == "unknown": risk["risk"] = "high"
|
||||
risk["recommend_slippage"] = max(risk["recommend_slippage"], 20.0)
|
||||
|
||||
if volume > 0 and liquidity > 0 and volume / liquidity > 5:
|
||||
risk["warnings"].append("📊 Volume/Liquidity ratio high — possible manipulation")
|
||||
risk["recommend_slippage"] = max(risk["recommend_slippage"], 15.0)
|
||||
|
||||
if not risk["warnings"]:
|
||||
risk["risk"] = "low"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
MEV_RISK_CACHE[cache_key] = {**risk, "ts": time.time()}
|
||||
return risk
|
||||
|
||||
|
||||
async def _format_mev_warning(risk: dict) -> str:
|
||||
"""Format MEV risk warning for display."""
|
||||
if not risk["warnings"]:
|
||||
return ""
|
||||
emoji = {"critical": "🔴", "high": "🟡", "low": "🟢", "unknown": "⚪"}
|
||||
e = emoji.get(risk["risk"], "⚪")
|
||||
text = f"\n\n{e} <b>MEV Protection</b>\n"
|
||||
if risk["risk"] == "critical":
|
||||
text += "⚡ HIGH sandwich attack risk detected!\n\n"
|
||||
for w in risk["warnings"]:
|
||||
text += f" {w}\n"
|
||||
text += f"\n💡 Recommended slippage: {risk['recommend_slippage']}%"
|
||||
return text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue