Skip to content

Safety

This bot signs and broadcasts irreversible mainnet transactions. The defaults are conservative but not foolproof. Read this whole page before any change that touches executors, slippage, gas, approvals, or signers.

Last updated: 2026-05-13


1. The risk profile

What this bot can do badly if misconfigured: - Over-slip on removal — accept far less than fair value because MAX_SLIPPAGE_BPS is too high. - Burn gas in a fee spike — broadcast at any price because MAX_GAS_GWEI is too high. - Get sandwich-attacked — public-mempool exposure on Ethereum mainnet. - Approve a malicious LP token — interact with a fake pool. Mitigated by TOKEN_BLACKLIST and V2 honeypot pre-flight, but not eliminated. - Drain to the wrong addressEMERGENCY_WALLET is treated as trusted; if it's compromised, all positions get drained. - Get front-run on the /remove Telegram command — anyone with Telegram chat access can trigger a tx.

Treat the wallet running this bot as a hot wallet. Hold only what you're willing to lose to a worst-case bug.


2. Safety knobs (env vars)

Var Purpose Recommended starting value Notes
DRY_RUN Simulate only true for any new config Set to false only after ≥24h clean dry run
MAX_SLIPPAGE_BPS Max accepted slippage on removal 100 (1%) Tighten for stable pairs (e.g., 30); loosen only for very thin pools
MAX_GAS_GWEI Hard cap; tx aborts above this 150 mainnet, 5 Base Tx aborts before broadcast — does not retry until gas drops
GAS_PRIORITY_GWEI Priority fee 2 mainnet, 0.001 Base Too low → tx stuck pending
TX_DEADLINE_SEC V2 removeLiquidity deadline 600 Tx reverts if not mined within this window — protects against stale prices
MIN_LIQUIDITY_USD Skip dust positions 50 Below this, gas exceeds redeemable value
TOKEN_BLACKLIST Comma-separated lowercase addresses; never interacted with tokens you know are scams Per-executor check; mirror in any new executor
AUTO_REVOKE_APPROVALS Set LP allowance → 0 after removal true Cheap insurance against future router exploits
MAX_TX_PER_MINUTE Token-bucket cap 3 Cascading triggers cannot run away
MAX_TX_PER_HOUR Same 20 Same
EMERGENCY_WALLET Dead-man's switch wallet dedicated cold address See §3

3. Emergency wallet

A dead-man's switch. Designate a wallet other than the one running this bot. If you ever spot trouble (suspected key compromise, anomaly on the pool, you're away from a keyboard), send any transaction from the emergency wallet — even a 1-wei self-transfer. The bot, on seeing that tx confirmed in a block, fires removal on every position immediately.

How it works internally

  • eventMonitor.js subscribes to new blocks and scans block.prefetchedTransactions[*].from.
  • If from.toLowerCase() === EMERGENCY_WALLET.toLowerCase(), it emits an emergency trigger to the engine.
  • The trigger engine bypasses all other predicates and dispatches removal for every position.

Trade-offs

Property Value
Latency ~12 s (one block confirmation)
False positives Low (you control the wallet)
Mempool spoofing risk None — we only check confirmed inclusion
MEV race None — by the time we see the tx, it's already mined
Cost to trigger One trivial tx fee from the emergency wallet

Operational notes

  • The emergency wallet must hold a small amount of native gas. Refill periodically.
  • Keep its private key on hardware or cold storage.
  • Do not reuse this wallet for anything else.
  • Test it: in dry-run mode, send a tx from the emergency wallet and confirm the bot logs emergency trigger fired for every position.

4. Signers and keys

Priority order in 📁 src/security/keystore.js:

  1. Ledger — preferred for non-trivial position size. Every removal tx requires a physical button press, which kills any silent-drain attack but means you must be near the device.
  2. Encrypted keystore./keystore.json produced by npm run encrypt-key. Password protects the key at rest. Suitable for VPS / Docker deployments.
  3. Raw private keyEVM_PRIVATE_KEY in .env. Refused in NODE_ENV=production. Dev-only.

Rules

  • Never commit .env or keystore.json. Both are in .gitignore — keep it that way.
  • Rotate keys after any suspected exposure (you pasted in chat, plugged USB into untrusted host, etc.).
  • Use a dedicated wallet for this bot. Do not use your main wallet.
  • Solana has no Ledger / keystore support yet — use a separate hot wallet, minimal SOL.

5. Gas cap behavior

utils/gas.js::gasAboveCap() is called inside every EVM executor before the broadcast. If (baseFee + priorityFee) > MAX_GAS_GWEI:

  • Tx is not broadcast.
  • Logged as a warning, not an error.
  • The trigger remains in firing state until the executor returns; on return, the engine calls markFailed, which clears firing. The next trigger event will re-evaluate.

This is intentional — the bot does not retry until you drop the cap or gas drops. During severe congestion, you may need to manually raise MAX_GAS_GWEI and restart.


6. Slippage math

utils/slippage.js::applySlippage(amount, bps) is bigint-only. Mixing Number and bigint will throw at runtime. Reads:

amountMin = amount - (amount * BigInt(bps)) / 10000n
  • bps=0 → no slippage tolerance (often reverts; use only on stable pairs in calm conditions).
  • bps=10000 → 100% slippage; accepts any amount. Never use this in prod.
  • V3 derives amount0Min/amount1Min from a decreaseLiquidity.staticCall so the slippage is applied to a realistic expectation, not the theoretical max.

7. MEV

On Ethereum mainnet, the public mempool is hostile. Any sufficiently large removeLiquidity tx may get sandwiched.

Mitigations the bot already provides: - Slippage cap (MAX_SLIPPAGE_BPS) — sandwicher cannot extract more than this. - Honeypot pre-flight (V2) — staticCall simulation aborts if the pool refuses removal.

Mitigations you must add yourself for non-trivial size: - Use a private mempool RPC — Flashbots Protect, MEV Blocker, or BloXroute. Set ETH_RPC_HTTP to the private endpoint; keep ETH_RPC_WS on a public endpoint for monitoring. - Split large positions into multiple smaller LP tokens and stagger removal — reduces the per-tx attacker payoff. - Avoid trigger thresholds that everyone else uses (e.g., round 10%/20%/30% drops) — those are easy to front-run.

L2 / Solana are less hostile but not immune; the same private-RPC pattern helps where available.


8. Logging

pino logger in 📁 src/logger.js redacts on two layers:

  1. Key-name redaction — the SENSITIVE_KEYS list (private keys, keystore password, bot token, webhook URL, CoinGecko key, and every *_RPC_* URL, since those carry provider API keys) is blanked wherever it appears, up to 4 levels of nesting.
  2. Value scrubbing serializersurl/rpc/endpoint fields are masked to host-only via 📁 src/utils/redact.js::maskUrl; err/error/reason strings are run through scrubSecrets, which masks any embedded 64+‑char hex (private keys), base58 secret blobs, and key-bearing URLs.

Rules

  • If you add a new sensitive env var, add it to SENSITIVE_KEYS. Covered by 📁 tests/redact.test.js for the helpers, but the key list itself relies on code review.
  • Never log a full RPC URL, Wallet, or Keypair. Log the masked host ({ rpc } is auto-masked) or maskAddress(signer.address).
  • Default log level for production is info. debug/trace may emit pool addresses + amounts that, while not secrets, leak strategy.

8a. Privacy & identity

The goal: nothing links your operator identity (wallet address, IP) to your activity beyond what is unavoidably public on-chain.

Vector Exposure Mitigation
RPC provider Sees your IP + every address/position you query SOCKS_PROXY=socks5h://127.0.0.1:9050 (Tor) or HTTPS_PROXY — routes all egress including the ethers RPC HTTP + WebSocket and the Solana RPC HTTP through the proxy (verified: the RPC host is tunnelled, not connected directly). Solana account-subscription WS uses the SDK's own socket — for that, add OS-level Tor/VPN.
CoinGecko (PnL) Reveals which tokens you hold to a third party Off by default (PRICE_LOOKUP_ENABLED=false) — only affects cosmetic USD PnL, never triggers. Set true to opt in
Dashboard Serves wallet address + positions, no auth Bound to 127.0.0.1 by default; wallet address masked unless DASHBOARD_SHOW_FULL_ADDRESS=true; security headers (CSP connect-src 'self', X-Frame-Options: DENY) block exfiltration/clickjacking. Never set DASHBOARD_BIND to a public interface without an authed proxy.
data/events.jsonl Plaintext record of positions/PnL/tx hashes on disk DASHBOARD_PERSIST=false disables it; data/ is git-ignored
Secret files at rest .env/keystore readable by other OS users ENFORCE_FILE_PERMS=true (default) refuses to start in production if they are group/world-readable; run chmod 600 .env keystore.json
Alerts (Telegram/Discord) The alert content (positions, PnL, tx hashes) passes through a third party, tying your chat identity to on-chain activity Inherent to using them; leave TELEGRAM_BOT_TOKEN/DISCORD_WEBHOOK_URL blank to opt out. All alert egress honors the proxy.

All egress shares one proxy agent (📁 src/utils/proxyAgent.js), so a single SOCKS_PROXY/HTTPS_PROXY setting covers ethers RPC (HTTP + WS), the Solana connection, and axios (alerts, price) at once.

The startup security preflight (📁 src/security/preflight.js) surfaces these as warnings at boot and blocks production start on critical file-permission issues. Fail-closed: a set-but-broken proxy aborts rather than sending traffic in the clear.

Minimal setup for full IP privacy

  1. Run Tor locally (e.g. brew install tor && tor, listening on 9050).
  2. In .env: SOCKS_PROXY=socks5h://127.0.0.1:9050
  3. Boot — you should see All egress (RPC + HTTP) routed through SOCKS proxy — IP hidden.

That routes the RPC connection, alerts, and price lookups through Tor. No other change is needed; automation is unaffected.

Unavoidable: every removal is a public mainnet transaction from your wallet. Privacy here means not additionally leaking your IP/identity through the bot's own machinery — it cannot make on-chain activity anonymous. For that, fund the wallet privately and use a private mempool (see §7 MEV).


9. Blacklist

TOKEN_BLACKLIST is enforced per executor, not globally registered. Every executeXxxRemoval calls _assertNotBlacklisted([token0, token1]) against config.blacklist before submitting.

When adding a new DEX, copy this pattern. If you forget, blacklist entries silently do nothing on that DEX.

What to put on the blacklist

  • Known scam tokens / rugs you've identified.
  • Tokens with fee-on-transfer mechanics that break LP math.
  • Tokens that have been used as honeypots against pools you watched.
  • Stale tokens whose contracts have been paused or had ownership transferred to suspicious addresses.

10. Pre-deploy safety checklist

Run through this every time you change executors, signers, chains, or risk parameters:

  • Unit tests green (npm run test:run)
  • Lint clean (npm run lint)
  • Audit reviewed (npm run audit)
  • .env reviewed line-by-line — no secrets in plain text where avoidable
  • DRY_RUN=true until validated
  • MAX_SLIPPAGE_BPS set per pool type (tight for stable, looser for volatile, never above ~300 bps without explicit reason)
  • MAX_GAS_GWEI set per chain
  • EMERGENCY_WALLET set and funded with gas
  • TOKEN_BLACKLIST reviewed
  • Signer = Ledger or encrypted keystore (raw key blocked in production — but verify)
  • Logger redaction list still covers any new env vars
  • Telegram alerts test-pinged
  • ≥24 h continuous dry run, no unhandled errors
  • Sensitivity probe completed (forced trigger, verified executor logged correct tx)
  • Manual /remove test completed in dry-run
  • One person standing by for the first hour live

If you can't tick every box, stay on dry-run.