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 address — EMERGENCY_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.jssubscribes to new blocks and scansblock.prefetchedTransactions[*].from.- If
from.toLowerCase() === EMERGENCY_WALLET.toLowerCase(), it emits anemergencytrigger 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 firedfor every position.
4. Signers and keys¶
Priority order in 📁 src/security/keystore.js:
- 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.
- Encrypted keystore —
./keystore.jsonproduced bynpm run encrypt-key. Password protects the key at rest. Suitable for VPS / Docker deployments. - Raw private key —
EVM_PRIVATE_KEYin.env. Refused inNODE_ENV=production. Dev-only.
Rules¶
- Never commit
.envorkeystore.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
firingstate until the executor returns; on return, the engine callsmarkFailed, which clearsfiring. 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:
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/amount1Minfrom adecreaseLiquidity.staticCallso 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:
- Key-name redaction — the
SENSITIVE_KEYSlist (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. - Value scrubbing serializers —
url/rpc/endpointfields are masked to host-only via📁 src/utils/redact.js::maskUrl;err/error/reasonstrings are run throughscrubSecrets, 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.jsfor the helpers, but the key list itself relies on code review. - Never log a full RPC URL,
Wallet, orKeypair. Log the masked host ({ rpc }is auto-masked) ormaskAddress(signer.address). - Default log level for production is
info.debug/tracemay 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¶
- Run Tor locally (e.g.
brew install tor && tor, listening on 9050). - In
.env:SOCKS_PROXY=socks5h://127.0.0.1:9050 - 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) -
.envreviewed line-by-line — no secrets in plain text where avoidable -
DRY_RUN=trueuntil validated -
MAX_SLIPPAGE_BPSset per pool type (tight for stable, looser for volatile, never above ~300 bps without explicit reason) -
MAX_GAS_GWEIset per chain -
EMERGENCY_WALLETset and funded with gas -
TOKEN_BLACKLISTreviewed - 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
/removetest completed in dry-run - One person standing by for the first hour live
If you can't tick every box, stay on dry-run.