Cyclic arbitrage in decentralized crypto exchanges

Cyclic Arbitrage: Finding Money in Token Loops!!!

cycle.png

Table of Contents

  1. Introduction and Strategy Overview
  2. The Graph Theory Behind It
  3. Data Collection from Uniswap V2
  4. Sampling-Based Arbitrage Detection
  5. Implementation and Results
  6. Challenges and Limitations

1. Introduction and Strategy Overview

In this strategy, I hunt for arbitrage opportunities in cryptocurrency markets by looking for profitable cycles in token exchange rates. The idea is simple: if you can trade Token A -> Token B -> Token C -> Token A and end up with more Token A than you started with, you've got some nice arbitrage!!!

According to efficient market hypothesis or whatever, these opportunities shouldn't exist for long, but in the wild world of DeFi, they pop up all the time due to:

  • Slippage and market impact
  • Network congestion causing price delays
  • Liquidity imbalances across exchanges
  • MEV bots and other big players competing

1.1 Why Cyclic Arbitrage?

Unlike traditional arbitrage between spot and futures, this strategy exploits inefficiencies within the same market (Uniswap V2). The beauty is that you don't need to predict market direction which is nice because I've had a hard time with that.

1.2 Strategy Setup

We define a profitable cycle as any sequence of trades where:

Profit = (Rate_A->B × Rate_B->C × Rate_C->A) > 1.0

Where each rate includes the 0.3% Uniswap V2 fee. If this product is greater than 1, we've found an arbitrage opportunity!

2. The Graph Theory Behind It

The core insight is that we can model all token pairs as a directed graph where:

  • Nodes = Tokens
  • Edges = Exchange rates between tokens
  • Edge weights = The actual exchange rate (including fees)

2.1 Building the Graph

I use NetworkX to construct a massive graph from Uniswap V2 data:

G = nx.DiGraph()
# add nodes for each token
G.add_node(token0, symbol=symbol0, decimals=dec0)
G.add_node(token1, symbol=symbol1, decimals=dec1)

# add edges with exchange rates
G.add_edge(token0, token1, rate=rate0_to_1)
G.add_edge(token1, token0, rate=rate1_to_0)

2.2 Finding Cycles

The arbitrage detection boils down to finding cycles in this graph where the product of edge weights is greater than 1.0. This is exactly what nx.simple_cycles() does, but we need to be smart about it because:

  • A graph with 10,000 tokens has potentially billions of cycles
  • Most cycles aren't profitable anyway
  • We need to filter out suspicious cycles (like 10x profits that are probably data errors)

3. Data Collection from Uniswap V2

3.1 The Graph Protocol

I use The Graph's hosted service to query Uniswap V2 data. This gives me real-time access to:

  • All trading pairs
  • Reserve amounts for each token
  • Total supply of LP tokens
  • USD values of reserves

3.2 Token Metadata

The tricky part is handling different token decimals. A token with 18 decimals vs 6 decimals needs completely different normalization:

norm_reserve0 = reserve0 / (10 ** int(dec0))
norm_reserve1 = reserve1 / (10 ** int(dec1))

3.3 Rate Calculation

For each pair, I calculate the exchange rate as:

rate = (reserve_out / reserve_in) * (1 - 0.003)

The 0.003 factor accounts for the Uniswap V2 fee. This gives us the actual rate we'd get if we traded.

4. Sampling-Based Arbitrage Detection

With 10,000+ tokens, checking every possible cycle is computationally intractible. Even a 3-token cycle has 10,000^3 = 1 trillion possibilities.

4.2 Smart Sampling Strategy

Instead of checking everything, I use a sampling approach that focuses on:

  • High-degree tokens: Tokens with many trading pairs are more likely to be part of arbitrage cycles
  • Weighted sampling: More important tokens get sampled more frequently
  • Time limits: Stop after 5 seconds to keep things fast

4.3 The Algorithm

def find_sampled_arbitrage(graph, min_profit=1.001, max_samples=5000):
    # focus on high-degree tokens
    important_tokens = get_top_tokens_by_degree(graph, top_n=100)

    # sample random cycles starting from important tokens
    for _ in range(max_samples):
        token_a = sample_weighted_by_degree(important_tokens)
        token_b = random_neighbor(token_a)
        token_c = random_neighbor(token_b)

        # check if profitable
        profit = calculate_cycle_profit([token_a, token_b, token_c])
        if profit > min_profit:
            record_arbitrage(cycle, profit)

5. Implementation and Results

5.1 The Main Classes

SwapGraph: Handles data collection and graph building

  • Fetches data from The Graph
  • Normalizes token amounts
  • Builds the NetworkX graph

CyclicArb: Manages arbitrage detection

  • Runs the sampling algorithm
  • Filters and reports results
  • Stores historical arbitrage data

5.2 Example Output

When I run the scanner, I get output like:

USDC -> WETH -> USDT -> USDC | Profit = 1.0023
WETH -> USDT -> DAI -> WETH | Profit = 1.0018

These are (seemingly) real arbitrage opportunities! (hint: this is when I found out about bad liquidity pools and dead tokens)

5.3 Performance Metrics

  • Graph size: ~10,000 tokens, ~50,000 edges
  • Scan time: ~5 seconds per scan
  • Arbitrage frequency: 5-20 opportunities per scan
  • Average profit: 0.1-0.5% per cycle

6. Challenges and Limitations

6.1 Slippage Reality

The theoretical profits I find don't account for:

  • Price impact: Large trades move the market
  • Gas costs: Ethereum transactions aren't free
  • MEV competition: Other bots are hunting the same opportunities

6.2 Data Quality Issues

Sometimes the data is weird:

  • Tokens with 0 decimals (causing division by zero)
  • Pairs with $0 liquidity (obviously not tradeable)
  • Malformed token addresses

6.3 Scalability Problems

The current implementation has limits:

  • Only samples 5,000 cycles per scan
  • Focuses on top 100 tokens by degree
  • 5-second time limit per scan

6.4 The MEV Problem

Even if I find profitable cycles, executing them is hard:

  • Need to bundle transactions to avoid front-running
  • Gas costs can eat all the profit
  • Other arbitrageurs are competing for the same opportunities

All this is to say:

Cyclic arbitrage is a fascinating form of arbitrage in DeFi markets, and this allowed me to learn a lot about DeFi programming in general, but this did not end up being profitable due to (I believe):

  • lack of computational power and speed
  • lack of variety of DeFi exchanges (more LPs, more arbing possibilities/cycles combos)
  • slow algorithms with not enough of an exhaustive search.
  • competition with MEV bots.

I learned a lot though!!!

Comments

No comments yet.