Skip to content

0xMars42/base-arb-scanner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

base-arb-scanner

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.

CI License: MIT


What it does

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:

  1. If the spot spread exceeds a configurable threshold,
  2. it triggers an eth_call to the relevant DEX's Quoter contract
  3. to compute the exact P&L the swap would yield (slippage included),
  4. 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.

Demo output

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_call fires 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.

Architecture

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 raw slot0 mid 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, no Box<dyn> / async-trait). Every block reads all slot0s in a single Multicall3 eth_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.
  • tokio current_thread runtime — our workload is I/O-serial; the multi-thread runtime would force Send bounds and complicate the Dex trait 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.

Quick start

Requires Rust 1.95+ and Cargo.

git clone https://github.com/0xMars42/base-arb-scanner.git
cd base-arb-scanner
cargo run --release

Works 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).

Configuration

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

Quality

  • cargo fmt --check clean
  • clippy::pedantic + clippy::nursery enabled crate-wide, zero warnings (the few allows are documented — deliberate f64 display 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 MBlto = "fat", codegen-units = 1, strip, and trimmed alloy / tokio feature sets (no "full")
cargo fmt --check
cargo clippy --all-targets -- -D warnings   # pedantic + nursery, zero warnings
cargo test
cargo build --release

Roadmap

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)

License

MIT. See LICENSE.

Author

0xMars42 — built as a portfolio project for roles in Rust / EVM infrastructure / MEV / crypto-quant.

About

Real-time cross-DEX arbitrage scanner for Base mainnet, written in Rust

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages