Skip to content

flashbots/simple-cyclic-arb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

simple-cyclic-arb

A minimal, readable cyclic (triangular) arbitrageur on Uniswap V2, written in Rust. It is a reference searcher workload: the smallest thing that still does what a real searcher does — read pool state every block, search for the profit-maximising trade size across a set of cycles, and submit the winner.

It is deliberately not a production system. There is no proprietary strategy here, no private orderflow, no exotic optimiser — just the textbook algorithm, laid out to be read and reproduced. MIT-licensed.

Why this exists

This repo was written to give researchers a realistic, self-contained searcher binary to run and study — for example, work on TEE / TDX side-channel security, where you want a representative MEV workload running inside an enclave to see how much of its behaviour leaks to the host. The properties that make it useful for that:

  • Compiled, single hot loop. The interesting compute is one bounded, integer-only optimiser (src/arb.rs) called once per cycle per block. Its shape is fixed; only the data changes. Real searchers look like this.
  • Deterministic and data-dependent. Same reserves in → same control flow and same number of iterations out. The optimizer_iterations knob lets you widen or narrow the loop to taste.
  • No hidden dependencies. It talks plain JSON-RPC to any node and, by default, sends nothing. You can run it read-only against mainnet forever.

The algorithm

Each block the bot does, for every configured cycle:

  1. Read reserves. getReserves() on each Uniswap V2 pair in the cycle, then orient them so reserve_in/reserve_out line up with the trade direction.
  2. Search for the optimal input. Profit as a function of trade size, f(x) = cycleOutput(x) − x, is concave for a chain of constant-product pools. A golden-section search over [1, reserve_in₀ / 2] converges to the maximum in optimizer_iterations steps. (src/arb.rs)
  3. Price each candidate by walking the input through every hop with the exact on-chain getAmountOut formula (0.30% fee, integer floor division). (src/amm.rs)
  4. Threshold. Keep the best cycle; if its profit clears min_profit_wei, act on it.
  5. Act. Dry-run (default): log it. Live: submit executeArb(...) to the on-chain CyclicArbExecutor, which performs the whole cycle atomically and reverts unless the closing balance grew by at least minProfit.

A production searcher swaps step 2 for something sharper (Brent, Nelder-Mead, or the closed form for two hops), watches the mempool instead of polling, prices many more venues, and submits via a builder/relay bundle rather than a public transaction. None of that changes the shape above.

Build

Needs a recent stable Rust toolchain.

cargo build --release
cargo test            # unit tests for the AMM math and the optimiser

The release profile keeps LTO off, uses a single codegen unit, and emits line tables — the binary stays easy to profile and disassemble.

Run (dry-run, no key, no funds)

cp config.example.toml config.toml
# edit rpc_url if you like; the default is a public mainnet endpoint
cargo run --release -- --config config.toml

You'll see one scan per block: the reserves fetched, the best cycle, its optimal size and profit, and — for anything above the threshold — a DRY-RUN: would submit arb line. --once scans a single block and exits. Set RUST_LOG=debug for more detail.

The bundled cycle (WETH → USDC → DAI → WETH) rarely clears a real edge on mainnet — that's expected. To see the submit path fire, lower min_profit_wei, or point the config at pools you control on a testnet.

Live mode

For an end-to-end test on a testnet or your own node:

  1. Deploy the executor and note its address:
    forge build
    forge create contracts/CyclicArbExecutor.sol:CyclicArbExecutor \
        --rpc-url <rpc> --private-key <key>
  2. Fund the executor with some of the start token (e.g. WETH).
  3. In config.toml set private_key, executor, and repoint rpc_url / chain_id / the pool addresses at your environment.
  4. Run with --live:
    cargo run --release -- --config config.toml --live

The bot signs and sends executeArb straight to the configured RPC's mempool. The transaction is atomic and self-checking: it reverts unless the cycle nets at least min_profit_wei, so a stale or unprofitable opportunity costs only gas.

If --live is set but private_key/executor are missing, the bot logs a warning and stays in dry-run.

Layout

src/
  main.rs     block loop: fetch reserves, optimise each cycle, log or submit
  arb.rs      golden-section search for the optimal trade size (the workload)
  amm.rs      Uniswap V2 getAmountOut / cycle pricing (the hot kernel)
  config.rs   TOML config + cycle validation
contracts/
  CyclicArbExecutor.sol   atomic multi-hop executor (reference, unaudited)
config.example.toml       WETH -> USDC -> DAI -> WETH on mainnet

Disclaimer

Educational reference code. Unaudited. Do not point the live path at real funds you are not prepared to lose. No warranty (MIT).

About

Minimal reference cyclic (triangular) arbitrageur on Uniswap V2 — a representative searcher workload for TEE/TDX side-channel research

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors