Real-time cross-DEX arbitrage scanner for Base mainnet, written in Rust. WebSocket-driven, with two-stage filtering and slippage-accurate validation via on-chain Quoter contracts.
Watches three liquid WETH/USDC pools on Base in real-time and surfaces arbitrage opportunities net of fees and gas:
- Uniswap V3 (5 bps fee tier) — most liquid major pair
- Uniswap V3 (30 bps fee tier) — less liquid, often diverges first
- Aerodrome Slipstream (dynamic fee, gauge-driven) — CLMM fork of Uni V3
On every new block (received via eth_subscribe WebSocket — no polling),
it reads spot prices in parallel, identifies the best buy/sell pair, and:
- If the spot spread exceeds a configurable threshold,
- it triggers an
eth_callto the relevant DEX's Quoter contract - to compute the exact P&L the swap would yield (slippage included),
- with dynamic gas pricing from
eth_gasPrice, cached with TTL.
This is the same two-stage filtering pattern real MEV searchers use: cheap detection → expensive validation only when worth it.
INFO Config chargee notional_weth=1.0 gas_units=300000 simulate_threshold_usdc=0.0
INFO RPC Base WebSocket connecte ws=wss://base-rpc.publicnode.com
INFO DEX 1 OK dex="UniswapV3 WETH/USDC 5bps" pool=0xd0b53D...F224
INFO DEX 2 OK dex="UniswapV3 WETH/USDC 30bps" pool=0x6c561B...1372
INFO DEX 3 OK dex="Aerodrome SS WETH/USDC 5bps" pool=0xb2cc22...DC59
INFO Subscription `newHeads` active
INFO spot analyse block=46598376 buy="Uni V3 30bps" sell="Aerodrome SS 5bps"
spread_bps="13.44"
prices="Uni V3 5bps=$1997.10 | Uni V3 30bps=$1996.00 | Aerodrome SS 5bps=$1998.68"
spot_net_usdc="$-4.31" gas="$0.0036"
# spot net < 0 -> two-stage filter SKIPS the Quoter: no eth_call wasted.
# (Set BAS_SIMULATE_THRESHOLD_USDC=-100 to force the precise Quoter path and
# watch slippage on near-misses:)
INFO Quoter: spread reel insuffisant (slippage mange le gain)
gross_sim="$-4.4958" net_sim="$-4.4994"
What the recruiter / reader sees here:
- WebSocket subscription (not polling) — pro-grade, no rate limit
- 3 DEXes in parallel via
tokio::join!— one RPC round-trip per block, and all 3 spot prices are logged (prices=...) so nothing is hidden - Break-even-gated two-stage filter — the precise (slippage-aware) Quoter
eth_callfires only when the cheap spot estimate already clears break-even. Since spot net is an optimistic upper bound on the realized net, a negative spot net guarantees a Quoter loss — so in the arbitraged steady state we make zero wasted validation calls - Real gas in USDC (~$0.004 on Base) — not a static guess
- Spread visible but unprofitable — exactly the expected steady state: the MEV bots already in place arbitrate continuously, so residual spreads stay below the round-trip cost. Detecting profitable arbs would require faster infra (private mempool, co-location) or rarer market dislocations.
src/
├── lib.rs # crate root
├── main.rs # WebSocket loop, two-stage filtering, dispatch
├── chain.rs # HTTP + WebSocket providers (alloy)
├── config.rs # env-driven Config, validated (BAS_* vars)
├── price.rs # sqrtPriceX96 -> human + raw/U256 conversions
├── gas.rs # GasOracle (eth_gasPrice + TTL cache) + pure math
├── multicall.rs # Multicall3 batching helper
├── arb.rs # analyze() (spot) + ArbSimulation (Quoter)
└── dex/
├── mod.rs # Dex trait + PoolQuote + Pool enum (N-pool dispatch)
├── uniswap_v3.rs # Uni V3 pool + QuoterV2 integration
└── aerodrome.rs # Slipstream pool + Slipstream Quoter
Design choices worth noting:
- Fee-adjusted arb detection — the buy/sell pair is selected on
effective execution prices (
mid × (1 ± fee)), not rawslot0mid prices. A pool with the lowest mid but a 30 bps fee is not the cheapest to buy from; comparing mids would systematically pick the high-fee pool and miss real arbs on the low-fee pairs. Selecting argmin(effective buy) / argmax(effective sell) yields the net-maximising pair. - Scales to N pools, compared in one pass — the pool set is a
Vec<Pool>(a static-dispatch enum, noBox<dyn>/async-trait). Every block reads allslot0s in a single Multicall3eth_call, then finds the best pair by argmin/argmax over effective prices — O(n), not pairwise O(n²). Adding a venue is one line; the Quoter dispatch is O(1) by index. - Pure functions where possible (
analyze,gas_cost_usdc,ArbSimulation::from_quoter_amounts) for trivial unit testing without any network mocking. tokiocurrent_threadruntime — our workload is I/O-serial; the multi-thread runtime would forceSendbounds and complicate theDextrait for zero practical benefit.- Generic over the provider (
P: Provider + Clone) — same code paths work with HTTP or WebSocket transports. - Dynamic on-chain values read at startup (Aerodrome fee, tickSpacing) — Aerodrome fees are gauge-driven (veAERO votes), so a hardcoded constant would silently drift.
Requires Rust 1.95+ and Cargo.
git clone https://github.com/0xMars42/base-arb-scanner.git
cd base-arb-scanner
cargo run --releaseWorks out of the box on the public Base WSS endpoint (publicnode.com).
No API key needed for a default run.
For a stable production-grade setup, point BASE_WS_URL to a dedicated
RPC (Alchemy / QuickNode free tier).
All knobs are environment variables. Copy .env.example to .env to
override any default.
| Variable | Default | Purpose |
|---|---|---|
BASE_WS_URL |
wss://base-rpc.publicnode.com |
WebSocket RPC (for eth_subscribe) |
BASE_RPC_URL |
https://mainnet.base.org |
HTTP RPC (optional, helpers only) |
BAS_NOTIONAL_WETH |
1.0 |
Notional size tested per analysis (WETH) |
BAS_EST_GAS_USDC |
0.50 |
Static fallback if eth_gasPrice fails |
BAS_GAS_UNITS_PER_ROUND_TRIP |
300000 |
Gas estimate for buy+sell |
BAS_GAS_CACHE_SECS |
30 |
TTL for gas price cache |
BAS_SIMULATE_THRESHOLD_USDC |
0.0 |
Spot net profit (USDC) above which Quoter is called (break-even gate; negative = also simulate near-misses) |
RUST_LOG |
info |
tracing log level |
cargo fmt --checkcleanclippy::pedantic+clippy::nurseryenabled crate-wide, zero warnings (the fewallows are documented — deliberatef64display math casts and test-only float asserts)- 26 unit tests covering price math, gas math, arb logic and config parsing
- Release binary is 5.7 MB —
lto = "fat",codegen-units = 1,strip, and trimmedalloy/tokiofeature sets (no"full")
cargo fmt --check
cargo clippy --all-targets -- -D warnings # pedantic + nursery, zero warnings
cargo test
cargo build --release| Phase | Status | What |
|---|---|---|
| A | ✅ | Live WETH/USDC price on Uniswap V3 |
| Refactor | ✅ | Modular crate (chain / dex / price) |
| B | ✅ | Cross-pool arb detection + Config runtime |
| C | ✅ | Aerodrome Slipstream (cross-DEX) |
| D | ✅ | Dynamic gas pricing with TTL cache |
| E.1 | ✅ | WebSocket subscription (newHeads) |
| E.2 | ✅ | Quoter integration (two-stage filtering) |
| E.3 | ✅ | Multicall3 batching of spot quotes |
| F | 📋 | Historical persistence + opportunity stats |
| G | 📋 | Real searcher (Flashbots-style bundles, testnet first) |
MIT. See LICENSE.
0xMars42 — built as a portfolio project for roles in Rust / EVM infrastructure / MEV / crypto-quant.