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.
- 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_estimateGasfor 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
- 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
- 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
The bot uses TOML configuration files for complete customization without code changes.
[network]
rpc_url = "https://polygon-rpc.com" # Polygon RPC endpoint
chain_id = 137 # Polygon mainnet chain ID[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
[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 = 6Effects:
address: Used for pool token identification and price calculationdecimals: Critical for accurate price normalization between different token precisionssymbol: Displayed in all output messages and database records
[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 checksEffects:
trade_amount: Larger amounts may have different gas costs and slippagemin_profit_usd: Filters out small opportunities, saves computational resourcesloop_interval_seconds: Balance between responsiveness and API rate limits
[database]
url = "postgresql://user:pass@host:port/database"# 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# 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- Rust 1.75+
- PostgreSQL 12+
- Git
- Clone and Build
git clone <repository-url>
cd polygon_price
cargo build --release- Database Setup
# Install PostgreSQL and create database
createdb arbitrage_db
# Run migrations
psql arbitrage_db < migrations/001_create_arbitrage_table.sql- Configuration
# For local development
cp config.local.toml config.toml
# For Docker deployment
cp config.example.toml config.toml- Insert Test Data
# Insert dummy data for testing
cargo run --bin insert_dummy- Run the Bot
# Development mode with logging
RUST_LOG=info cargo run
# Production mode
./target/release/polygon_price// 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
// 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/token0ratio - Decimal normalization accounts for different token precisions (WETH: 18, USDC: 6)
// 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
// 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
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)
}// 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_estimateGasfor accurate transaction simulation - Handles approval requirements dynamically
- Implements fallback estimates for network issues
- Accounts for gas refunds in unused gas calculations
// 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"));
};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%
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()))
}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()))
}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
);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?;
}- Connection Pooling: Persistent RPC and database connections
- Early Filtering: Skip expensive calculations for small price differences
- Async Operations: Concurrent API calls and database operations
- Efficient Serialization: Zero-copy deserialization with Serde
- Memory Management: Rust's ownership system prevents memory leaks
- Graceful Degradation: Fallback values for all external dependencies
- Retry Logic: Automatic retries for transient network failures
- Comprehensive Logging: Detailed error messages and execution traces
- Configuration Validation: Startup-time validation of all parameters
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%
Each profitable opportunity is stored with complete metadata for analysis and backtesting.
