Skip to content

Back to the catalog

Blind spots of a blind arb bot
collective.flashbots.netwriteup

Blind spots of a blind arb bot

Final installment of a five-part series reverse-engineering an unverified Arbitrum arbitrage bot purely from bytecode and on-chain behavior, explaining how it enters Uniswap V4's flash-accounting "unlock" window and nests V2/V3/V4 swaps so debts settle innermost-first with profit collected as the balancing entry. It details how the contract fingerprints a Uniswap V3 swap callback by calldata shape instead of reading its selector, and how its owner-only profit sweep derives recipient addresses at runtime via bit reversal, 160-bit rotation and XOR against a literal 0xdeadbeefcafebabe constant rather than storing them. The piece closes by cataloguing the bot's structural blind spots — pools outside its four-dialect switch, fee-on-transfer/rebasing tokens, and a documented missed arbitrage across unlisted pools during a large aggregator sell.

TL;DR Bytecode-level teardown of a blind Arbitrum MEV bot's Uniswap V4 flash-accounting settlement, selector-less callback fingerprinting, and obfuscated profit-sweep address derivation.
Document

What it is

Part 5 of the Flashbots Collective series reverse-engineering an unverified Arbitrum arbitrage bot. This installment covers the execution handoff into Uniswap V4's flash-accounting window, two "selector-avoidance" tricks (V3 callback fingerprinting and an obfuscated profit-sweep recipient), and a catalogue of the bot's structural blind spots.

How it works

The bot calls unlock() on the PoolManager, which calls back into unlockCallback (selector 0x91dd7346). Inside that single callback frame it walks hops in order: V4 hops trade natively against the PoolManager (only ledger deltas change, no tokens move), while V2/V3 hops require real ERC-20 transfers that the V4 ledger cannot see. Each pool's payment demand is answered by executing the next nested hop rather than paying immediately, so debts settle innermost-first; the final PoolManager.take() call extracts the leftover surplus as profit — profit collection and ledger-balancing are literally the same operation.

Two re-uses of the "no real selectors" theme from earlier parts:

  1. The contract never checks uniswapV3SwapCallback's selector. Instead it reads calldata word at byte 68 (must equal 0x60, the bytes-argument offset forced by the ABI) and byte 100 (must equal 0x80, the length the bot itself set when it made the swap call) — two CALLDATALOADs replace a selector check.
  2. The payload the contract sends itself via unlock() uses a different internal wire format than the public calldata grammar: 37 bytes/hop (vs. 39 externally), pool_type moved to byte 0 for faster branching, tick_spacing dropped entirely, and the pool address left unmasked (the XOR masking from earlier parts only defends against external mempool readers, not internal memory).

The owner-only profit sweep (selector 0x8e16402f) computes two recipient addresses at call time rather than storing them: reverse the low 80 bits of a baked-in constant via an 80-iteration bit-peeling loop, OR back under the untouched high bits, add an offset, then rotate the 160-bit result (shift amounts summing to 160 — e.g., <<137 | >>23 for one recipient, <<7 | >>153 for the other), and finally XOR against a literal PUSH8 0xdeadbeefcafebabe. One recipient receives native ETH (full contract balance), the other receives ERC-20 tokens via transfer. The author deliberately withholds the resolved addresses.

Preconditions / Affected

Applies to pools matching one of exactly four dialects the bot's price-reading switch understands: constant-product V2, concentrated-liquidity V3, Algebra, and V4. Any other AMM model (Curve-style stableswap, non-50/50 Balancer, order books, newer designs) is structurally unreadable — there is no pool_type byte for it. Separately, fee-on-transfer and rebasing tokens break the design because settlement requires the exact simulated amount to arrive; any token that alters balances in transit causes the V4 ledger to end non-zero and the transaction to revert.

Caveats

The author documents one real missed opportunity: during a large aggregator sell split across six pools, the bot's route list covered only three, missing three pools that carried roughly 2.3% of the flow (the most illiquid tail, consistent with the route list being curated for the deepest pools). Several threads remain explicitly unresolved by the author: the off-chain wake-up trigger for the bot's sleep/burst cycle, the purpose of a synthesized 0x4200...0006 (OP Stack WETH predeploy) address used only in an XOR mask, and the exact numeric semantics of a profit_scale gating variable (direction proven, units not).

Organisations named here

  • 0xdeadbeefcafebabe
  • golden-section search
  • OP Stack WETH predeploy (0x4200...0006)
  • owner sweep selector (0x8e16402f)
  • unlockCallback (0x91dd7346 selector)

Technologies named here

  • Unverified arbitrage bot contract
  • Uniswap V4 PoolManager
  • Uniswap V3 pools
  • Uniswap V2 pools
  • Algebra pools
  • OP Stack WETH predeploy address
  • Uniswap v4
  • Uniswap v2
  • Algebra
  • Arbitrum
  • Uniswap V3

Techniques named here

  • Selector-less dispatch / data-as-instructions
  • XOR-masked pool addresses in calldata
  • Self-addressed inner payload re-encoding
  • Uniswap V4 flash accounting
  • Nested/self-funding flash-loan-less arbitrage loop
  • Runtime-computed payout address obfuscation
  • Golden-section search for optimal trade size

Threat actors named here

  • Unidentified arbitrage bot operator