Skip to content

Repository files navigation

Polygon Arbitrage Bot

A high-performance arbitrage detection and analysis bot for Polygon DEXs, built in Rust. This bot continuously monitors price differences between Uniswap V3 and QuickSwap, calculates profitable arbitrage opportunities with real-time gas estimation, and stores results in PostgreSQL.

Features

  • Multi-DEX Support: Uniswap V3 and QuickSwap (Uniswap V2) with extensible architecture
  • Dynamic Token Pair Trading: Configurable support for any ERC-20 token pair (WETH/USDC, WBTC/USDC, etc.)
  • Real-time Gas Estimation: Uses eth_estimateGas for accurate transaction cost calculations
  • External Price Feeds: Fetches POL prices from CoinGecko API with hardcoded fallback
  • Intelligent Filtering: Skips gas estimation when price differences are below profit thresholds
  • PostgreSQL Integration: Stores profitable opportunities with UUID primary keys and timestamps
  • Docker Support: Complete containerization with Docker Compose
  • TOML Configuration: All parameters configurable without code changes

Technology Stack

Core Dependencies

  • Rust 2021 Edition: Systems programming language for performance
  • Alloy 0.3: Ethereum library with full features for blockchain interactions
  • Tokio 1.0: Async runtime with full feature set
  • SQLx 0.8: PostgreSQL integration with compile-time query checking
  • Reqwest 0.11: HTTP client for external API calls
  • Serde 1.0: JSON/TOML serialization with derive macros
  • Eyre 0.6: Error handling and reporting
  • Chrono 0.4: Date/time handling with serde support
  • Rust Decimal 1.36: Precise decimal arithmetic for financial calculations
  • TOML 0.8: Configuration file parsing

External Integrations

  • CoinGecko API: POL/USD price feeds (with hardcoded fallback)
  • Polygon Gas Station: Gas price recommendations (with RPC fallback)
  • Polygon RPC: Blockchain state queries and gas estimation
  • PostgreSQL: Persistent storage with UUID primary keys

⚙️ Configuration System

The bot uses TOML configuration files for complete customization without code changes.

Network Configuration

[network]
rpc_url = "https://polygon-rpc.com"  # Polygon RPC endpoint
chain_id = 137                       # Polygon mainnet chain ID

DEX Configuration

[dex.primary]
name = "Uniswap V3"                                    # Display name
dex_type = "uniswap_v3"                               # Protocol type
router_address = "0xE592427A0AEce92De3Edee1F18E0157C05861564"  # Router contract
pool_address = "0x45dDa9cb7c25131DF268515131f647d726f50608"    # Pool contract
fee_tier = 500                                        # Fee in basis points (0.05%)

[dex.secondary]
name = "QuickSwap"
dex_type = "uniswap_v2"
router_address = "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"
pool_address = "0x853Ee4b2A13f8a742d64C8F088bE7bA2131f670d"
fee_percentage = 0.003                                # Fee as decimal (0.3%)

Effects:

  • dex_type: Determines price calculation algorithm (V3 uses sqrtPriceX96, V2 uses reserves)
  • fee_tier/fee_percentage: Used in profit calculations and gas estimation
  • Pool addresses must match the exact token pair being traded

Token Configuration

[tokens.base_token]
name = "Wrapped Ethereum"
symbol = "WETH"                                       # Used in display messages
address = "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619"  # Contract address
decimals = 18                                         # Token decimals for price calculations

[tokens.quote_token]
name = "USD Coin"
symbol = "USDC"
address = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
decimals = 6

Effects:

  • address: Used for pool token identification and price calculation
  • decimals: Critical for accurate price normalization between different token precisions
  • symbol: Displayed in all output messages and database records

Trading Parameters

[trading]
trade_amount = 1.0          # Base token amount for arbitrage calculations
min_profit_usd = 5.0        # Minimum profit threshold (USD)
loop_interval_seconds = 10  # Delay between price checks

Effects:

  • trade_amount: Larger amounts may have different gas costs and slippage
  • min_profit_usd: Filters out small opportunities, saves computational resources
  • loop_interval_seconds: Balance between responsiveness and API rate limits

Database Configuration

[database]
url = "postgresql://user:pass@host:port/database"

🐳 Docker Setup

Quick Start with Docker Compose

# Clone the repository
git clone <repository-url>
cd polygon_price

# Copy and customize configuration
cp config.example.toml config.toml
# Edit config.toml with your preferred settings

# Start the entire stack
docker-compose up -d

# View logs
docker-compose logs -f arbitrage_bot

Manual Docker Build

# Build the image
docker build -t polygon-arbitrage-bot .

# Run with custom config
docker run -v $(pwd)/config.toml:/app/config.toml polygon-arbitrage-bot

Local Development Setup

Prerequisites

  • Rust 1.75+
  • PostgreSQL 12+
  • Git

Installation Steps

  1. Clone and Build
git clone <repository-url>
cd polygon_price
cargo build --release
  1. Database Setup
# Install PostgreSQL and create database
createdb arbitrage_db

# Run migrations
psql arbitrage_db < migrations/001_create_arbitrage_table.sql
  1. Configuration
# For local development
cp config.local.toml config.toml

# For Docker deployment
cp config.example.toml config.toml
  1. Insert Test Data
# Insert dummy data for testing
cargo run --bin insert_dummy
  1. Run the Bot
# Development mode with logging
RUST_LOG=info cargo run

# Production mode
./target/release/polygon_price

System Architecture & Bot Logic

High-Level Architecture

System Design

Core Algorithm Flow

1. Initialization Phase

// Load configuration and validate all parameters
let config = Config::load()?;

// Initialize blockchain provider
let provider = ProviderBuilder::new().on_http(config.network.rpc_url.parse()?);

// Initialize DEX-specific metadata for both primary and secondary DEXs
let (decimals0, decimals1, token0, token1) = initialize_dex_info(&dex_config, pool_addr, &provider).await?;

Technical Details:

  • Validates all contract addresses and network connectivity
  • Fetches token decimals and addresses from pool contracts
  • Establishes persistent RPC connections for optimal performance

2. Price Fetching Engine

Uniswap V3 Price Calculation
// Fetch sqrtPriceX96 from pool's slot0
let slot0_result = pool.slot0().call().await?;
let sqrt_price_x96 = U256::from(slot0_result.sqrtPriceX96);

// Convert to human-readable price
let sqrt_price_x96_f64 = sqrt_price_x96.to::<u128>() as f64;
let q96 = 2f64.powi(96);
let sqrt_price = sqrt_price_x96_f64 / q96;
let raw_price = sqrt_price * sqrt_price;

// Normalize for token decimals
let price = 1.0 / (raw_price / 10f64.powi(decimals1 as i32 - decimals0 as i32));

Mathematical Foundation:

  • Uniswap V3 stores price as sqrtPriceX96 = sqrt(price) * 2^96
  • Price represents token1/token0 ratio
  • Decimal normalization accounts for different token precisions (WETH: 18, USDC: 6)
Uniswap V2 Price Calculation
// Fetch reserves from pair contract
let reserves = pair.getReserves().call().await?;
let base_token_addr = Address::from_str(&config.tokens.base_token.address)?;

// Calculate price based on token order
let price = if token0_addr == base_token_addr {
    (reserves.reserve1.to::<u128>() as f64 * 10f64.powi(decimals0 as i32 - decimals1 as i32)) / reserves.reserve0.to::<u128>() as f64
} else {
    (reserves.reserve0.to::<u128>() as f64 * 10f64.powi(decimals1 as i32 - decimals0 as i32)) / reserves.reserve1.to::<u128>() as f64
};

Technical Details:

  • Uses constant product formula: x * y = k
  • Price = quote_token_reserve / base_token_reserve
  • Handles token ordering automatically by comparing addresses

3. Intelligent Filtering System (Difference v/s min_profit_usd)

// Calculate raw price difference in USD terms
let price_diff_usd = (primary_price - secondary_price).abs() * config.trading.trade_amount;

// Early exit if difference is below threshold
if price_diff_usd < config.trading.min_profit_usd {
    println!("Price difference (${:.2}) below minimum threshold (${:.2}), skipping arbitrage analysis", 
             price_diff_usd, config.trading.min_profit_usd);
    continue;
}

Optimization Logic:

  • Prevents expensive gas estimation for obviously unprofitable opportunities
  • Saves ~200ms per iteration by avoiding blockchain calls
  • Reduces API rate limit consumption

4. Dynamic Gas Estimation Engine

Multi-Step Gas Calculation
async fn estimate_swap_gas(
    provider: &RootProvider<Http<Client>>,
    trade_amount: f64,
    is_primary_to_secondary: bool,
    config: &Config,
) -> Result<u64> {
    let mut total_gas = 0u64;
    
    // Estimate gas for both swaps in the arbitrage
    total_gas += estimate_dex_swap_gas(/* first swap */).await?;
    total_gas += estimate_dex_swap_gas(/* second swap */).await?;
    
    // Add approval gas if needed
    total_gas += estimate_approval_gas(/* approval checks */).await?;
    
    // Add 10% buffer for gas price fluctuations
    let buffered_gas = ((total_gas as f64) * 1.10).ceil() as u64;
    Ok(buffered_gas)
}
DEX-Specific Gas Estimation
// Uniswap V3 gas estimation
let params = ISwapRouter::ExactInputSingleParams {
    tokenIn: token_in,
    tokenOut: token_out,
    fee: Uint::from(fee_tier),
    recipient: user,
    deadline,
    amountIn: amount_in,
    amountOutMinimum: U256::ZERO,
    sqrtPriceLimitX96: Uint::ZERO,
};

if let Ok(gas_estimate) = router_contract.exactInputSingle(params).estimate_gas().await {
    Ok(gas_estimate as u64)
} else {
    Ok(150_000) // Conservative fallback
}

Technical Implementation:

  • Uses eth_estimateGas for accurate transaction simulation
  • Handles approval requirements dynamically
  • Implements fallback estimates for network issues
  • Accounts for gas refunds in unused gas calculations

5. Comprehensive Profit Calculation

Fee Structure Analysis
// Extract fees from configuration
let primary_fee = if let Some(fee_tier) = config.dex.primary.fee_tier {
    fee_tier as f64 / 1_000_000.0 // Basis points to decimal
} else if let Some(fee_percentage) = config.dex.primary.fee_percentage {
    fee_percentage
} else {
    return Err(eyre::eyre!("Primary DEX must have fee configuration"));
};
Arbitrage Profit Formula
let profit = if primary_price > secondary_price {
    // Strategy: Buy low (secondary), sell high (primary)
    let buy_cost = trade_amount * secondary_price * (1.0 + secondary_fee);
    let sell_revenue = trade_amount * primary_price * (1.0 - primary_fee);
    sell_revenue - buy_cost - gas_cost_usd
} else {
    // Strategy: Buy low (primary), sell high (secondary)
    let buy_cost = trade_amount * primary_price * (1.0 + primary_fee);
    let sell_revenue = trade_amount * secondary_price * (1.0 - secondary_fee);
    sell_revenue - buy_cost - gas_cost_usd
};

Economic Model:

  • Revenue: trade_amount × higher_price × (1 - selling_fee)
  • Cost: trade_amount × lower_price × (1 + buying_fee) + gas_cost_usd
  • Profit: Revenue - Cost
  • ROI: (Profit / Investment) × 100%

6. Real-Time Price Feed Integration

POL Price Fetching
async fn get_pol_price_usd() -> Result<(f64, String)> {
    let client = reqwest::Client::new();
    
    // Primary: CoinGecko API
    if let Ok(response) = client
        .get("https://api.coingecko.com/api/v3/simple/price?ids=polygon-ecosystem-token&vs_currencies=usd")
        .send()
        .await
    {
        if let Ok(data) = response.json::<CoinGeckoResponse>().await {
            if let Some(price) = data.pol.get("usd") {
                return Ok((*price, "CoinGecko API".to_string()));
            }
        }
    }
    
    // Fallback: Hardcoded value
    Ok((0.45, "Hardcoded fallback".to_string()))
}
Gas Price Optimization
async fn get_gas_price_gwei(provider: &RootProvider<Http<Client>>) -> Result<(f64, String)> {
    // Primary: Polygon Gas Station
    if let Ok(response) = client
        .get("https://gasstation.polygon.technology/v2")
        .send()
        .await
    {
        if let Ok(data) = response.json::<GasStationResponse>().await {
            return Ok((data.standard.max_fee, "Polygon Gas Station".to_string()));
        }
    }
    
    // Secondary: RPC provider
    if let Ok(gas_price) = provider.get_gas_price().await {
        return Ok((gas_price as f64 / 1e9, "RPC provider".to_string()));
    }
    
    // Fallback: Conservative estimate
    Ok((30.0, "Hardcoded fallback".to_string()))
}

7. Database Integration & Persistence

Opportunity Storage Schema
CREATE TABLE arbitrage_opportunities (
    id SERIAL PRIMARY KEY,
    token_pair VARCHAR(20) NOT NULL,           -- "WETH/USDC"
    dex_a VARCHAR(50) NOT NULL,                -- Lower price DEX
    price_a DECIMAL(20, 8) NOT NULL,           -- Lower price
    dex_b VARCHAR(50) NOT NULL,                -- Higher price DEX  
    price_b DECIMAL(20, 8) NOT NULL,           -- Higher price
    profit_token_amount DECIMAL(20, 8) NOT NULL, -- Trade amount
    profit_usd DECIMAL(10, 2) NOT NULL,        -- Profit in USD
    estimated_gas_usd DECIMAL(10, 4) NOT NULL, -- Gas cost
    trade_direction VARCHAR(100) NOT NULL,      -- "QuickSwap -> Uniswap V3"
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Conditional Storage Logic
if arbitrage_result.is_profitable && arbitrage_result.profit >= config.trading.min_profit_usd {
    let opportunity = ArbitrageOpportunity {
        token_pair: config.get_token_pair_string(),
        dex_a: if primary_price > secondary_price { &config.dex.secondary.name } else { &config.dex.primary.name }.to_string(),
        price_a: Decimal::from_f64_retain(if primary_price > secondary_price { secondary_price } else { primary_price }).unwrap_or_default(),
        // ... additional fields
    };
    
    database::save_arbitrage_opportunity(&pool, &opportunity).await?;
}

Performance Optimizations

  1. Connection Pooling: Persistent RPC and database connections
  2. Early Filtering: Skip expensive calculations for small price differences
  3. Async Operations: Concurrent API calls and database operations
  4. Efficient Serialization: Zero-copy deserialization with Serde
  5. Memory Management: Rust's ownership system prevents memory leaks

Error Handling & Resilience

  1. Graceful Degradation: Fallback values for all external dependencies
  2. Retry Logic: Automatic retries for transient network failures
  3. Comprehensive Logging: Detailed error messages and execution traces
  4. Configuration Validation: Startup-time validation of all parameters

Output Analysis

Console Output Example

WETH/USDC Price (Uniswap V3): 4597.343046 USDC
WETH/USDC Price (QuickSwap): 4595.123456 USDC
Price difference: 2.219590 USDC (0.05%)

Arbitrage Analysis (for 1.0 WETH):
Estimated gas cost: $0.234
Uniswap V3 fee: 0.050%, QuickSwap fee: 0.300%
[>] Data sources:
  POL price: CoinGecko API
  Gas price: Polygon Gas Station
[.] Profitable arbitrage: $1.85 profit
ROI: 0.04%

Database Records

Each profitable opportunity is stored with complete metadata for analysis and backtesting.

About

An arbitrage bot written in rust that fetches prices from 2 DEXs (Uniswap V3 and Quickstep, by default) and logs arbitrage opportunities into a postgres table (uses dynamic on-chain prices, gas cost, and a minimum profit threshold).

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages