ChainFusion: A Professional-Grade DEX Triangular Arbitrage System in Rust
2026-01-10
ChainFusion: A Professional-Grade DEX Triangular Arbitrage System in Rust
Decentralized exchanges are not perfectly efficient. Price discrepancies between trading pairs on the same DEX — or across DEXs — create fleeting arbitrage opportunities that last milliseconds. Capturing these opportunities requires systems that can detect, validate, and execute trades faster than the competition, all within a single atomic transaction.
ChainFusion is a professional-grade triangular arbitrage system built in Rust. It monitors 14 major liquidity pools, evaluates 26 arbitrage paths across 6 triangle combinations, and executes profitable trades using flash loans — requiring zero initial capital. In this post, I'll break down the architecture, the math, and the engineering behind ChainFusion-Arbitrage.
What is Triangular Arbitrage?
Triangular arbitrage exploits price inconsistencies across three trading pairs that form a cycle. Consider three tokens A, B, and C with the following prices:
A → B: 1 A = 2000 B (e.g., ETH → USDC)
B → C: 1 B = 1.01 C (e.g., USDC → DAI)
C → A: 1 C = 0.000505 A (e.g., DAI → ETH)
Starting with 1 ETH:
- Trade 1 ETH → 2000 USDC
- Trade 2000 USDC → 2020 DAI
- Trade 2020 DAI → 1.0201 ETH
Profit: 0.0201 ETH (~$50 at $2500/ETH)
In traditional finance, such discrepancies are rare and tiny. In DeFi, AMM pricing formulas (x*y=k) create mechanical price deviations after every swap, and fragmented liquidity across pools amplifies the effect. The key constraint is that all three trades must execute atomically — if any leg fails, the entire sequence must revert.
System Architecture
ChainFusion follows an event-driven architecture with clear separation between detection, validation, and execution:
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Pool Monitor │────▶│ Arbitrage Engine │────▶│ Executor │
│ (Event-Driven) │ │ (Path Scanner) │ │ (Flash Loans + │
│ │ │ │ │ Flashbots) │
│ • Swap events │ │ • 26 paths │ │ │
│ • Sync events │ │ • Profit calc │ │ • Bundle submit │
│ • Block headers │ │ • Gas estimation │ │ • MEV protection │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
└────────────────────────┼────────────────────────┘
│
┌─────────────┴─────────────┐
│ Smart Contracts │
│ FlashArbitrage.sol │
│ (Atomic execution) │
└───────────────────────────┘
Rust Workspace Structure
ChainFusion uses a Rust workspace with multiple crates for clean modularity:
chainfusion-arbitrage/
├── Cargo.toml # Workspace root
├── crates/
│ ├── scanner/ # Pool monitoring & event processing
│ │ ├── src/
│ │ │ ├── pools.rs # Pool state management
│ │ │ ├── events.rs # On-chain event parsing
│ │ │ └── lib.rs
│ │ └── Cargo.toml
│ ├── engine/ # Arbitrage detection & profit calculation
│ │ ├── src/
│ │ │ ├── paths.rs # Triangle path definitions
│ │ │ ├── profit.rs # Profit estimation
│ │ │ ├── quotes.rs # On-chain quote verification
│ │ │ └── lib.rs
│ │ └── Cargo.toml
│ ├── executor/ # Trade execution & MEV protection
│ │ ├── src/
│ │ │ ├── flashloan.rs # Flash loan integration
│ │ │ ├── flashbots.rs # Flashbots bundle submission
│ │ │ ├── gas.rs # Gas price strategy
│ │ │ └── lib.rs
│ │ └── Cargo.toml
│ ├── contracts/ # Solidity contracts & ABIs
│ │ ├── src/
│ │ │ └── FlashArbitrage.sol
│ │ └── Cargo.toml
│ └── api/ # REST API for strategy management
│ ├── src/
│ │ ├── routes.rs
│ │ └── lib.rs
│ └── Cargo.toml
├── backtest/ # Historical backtesting engine
└── config/ # Chain & pool configurations
This workspace structure allows each crate to be tested independently and compiled in parallel, significantly improving build times.
Pool Monitoring: 14 Pools, Real-Time
ChainFusion monitors 14 major liquidity pools across Uniswap V3, Uniswap V2, SushiSwap, and Curve. The scanner subscribes to on-chain Swap and Sync events via WebSocket, maintaining an in-memory snapshot of each pool's reserves:
pub struct PoolMonitor {
pools: Arc<DashMap<Address, PoolState>>,
provider: Arc<Provider<Ws>>,
}
#[derive(Clone, Debug)]
pub struct PoolState {
pub address: Address,
pub token0: Address,
pub token1: Address,
pub reserve0: U256,
pub reserve1: U256,
pub fee: u32, // In basis points
pub last_block: u64,
pub dex: DexType,
// V3-specific
pub sqrt_price_x96: Option<U256>,
pub tick: Option<i32>,
pub liquidity: Option<U128>,
}
impl PoolMonitor {
pub async fn start(&self, tx: mpsc::Sender<PoolUpdate>) -> Result<()> {
let filter = Filter::new()
.address(self.pool_addresses())
.event("Swap(address,uint256,uint256,uint256,uint256,address)")
.event("Sync(uint112,uint112)");
let mut stream = self.provider.subscribe_logs(&filter).await?;
while let Some(log) = stream.next().await {
let pool_addr = log.address;
if let Some(mut pool) = self.pools.get_mut(&pool_addr) {
pool.update_from_log(&log)?;
tx.send(PoolUpdate {
pool: pool_addr,
state: pool.clone(),
block: log.block_number.unwrap_or_default().as_u64(),
}).await?;
}
}
Ok(())
}
}
Every pool update triggers a scan of all relevant arbitrage paths that include the updated pool.
Triangle Paths: 6 Combinations, 26 Routes
The engine pre-computes all valid triangular paths from the monitored pools. With tokens like ETH, USDC, USDT, DAI, WBTC, and WETH across multiple DEXs, this produces 6 unique triangle combinations and 26 concrete routes (accounting for the same pair on different DEXs):
pub struct TrianglePath {
pub id: String,
pub legs: [Leg; 3],
pub base_token: Address, // Start and end token
}
pub struct Leg {
pub pool: Address,
pub token_in: Address,
pub token_out: Address,
pub dex: DexType,
}
// Example paths:
// ETH → USDC (UniV3 0.05%) → USDT (Curve 3pool) → ETH (UniV3 0.3%)
// ETH → USDC (UniV3 0.05%) → DAI (UniV2) → ETH (SushiSwap)
// WBTC → ETH (UniV3 0.3%) → USDC (UniV3 0.05%) → WBTC (UniV3 0.3%)
Profit Calculation: Dual-Layer Verification
Speed vs. accuracy is a fundamental tension in arbitrage. You need to evaluate opportunities in microseconds, but false positives waste gas on reverted transactions. ChainFusion solves this with a dual-layer verification approach:
Layer 1: Local Fast Estimation
The first pass uses the in-memory pool state to calculate expected output using the constant product formula (for V2) or the concentrated liquidity math (for V3):
pub fn estimate_profit(
path: &TrianglePath,
input_amount: U256,
pools: &DashMap<Address, PoolState>,
) -> Option<ProfitEstimate> {
let mut amount = input_amount;
for leg in &path.legs {
let pool = pools.get(&leg.pool)?;
amount = match pool.dex {
DexType::UniswapV2 | DexType::SushiSwap => {
calculate_v2_output(amount, &pool, leg.token_in == pool.token0)
}
DexType::UniswapV3 => {
calculate_v3_output(amount, &pool)
}
DexType::Curve => {
calculate_curve_output(amount, &pool)
}
};
}
let profit = amount.checked_sub(input_amount)?;
if profit > U256::zero() {
Some(ProfitEstimate {
path: path.id.clone(),
input: input_amount,
output: amount,
gross_profit: profit,
})
} else {
None
}
}
fn calculate_v2_output(amount_in: U256, pool: &PoolState, zero_for_one: bool) -> U256 {
let (reserve_in, reserve_out) = if zero_for_one {
(pool.reserve0, pool.reserve1)
} else {
(pool.reserve1, pool.reserve0)
};
let fee = U256::from(10000 - pool.fee); // e.g., 9970 for 0.3% fee
let amount_with_fee = amount_in * fee;
let numerator = amount_with_fee * reserve_out;
let denominator = reserve_in * U256::from(10000) + amount_with_fee;
numerator / denominator
}
This estimation runs in under 10 microseconds. If the estimated profit exceeds the gas cost threshold, we proceed to layer 2.
Layer 2: On-Chain Precise Quotation
The second pass uses eth_call to simulate the entire trade on-chain, getting exact output amounts that account for tick crossings (V3), slippage, and any state changes that occurred between our last event update:
pub async fn verify_on_chain(
estimate: &ProfitEstimate,
path: &TrianglePath,
provider: &Provider<Http>,
) -> Result<VerifiedProfit> {
let calldata = encode_multicall_quote(path, estimate.input);
let result = provider
.call(&TransactionRequest::new().to(QUOTER_ADDRESS).data(calldata), None)
.await?;
let actual_output = decode_quote_result(&result)?;
let gas_cost = estimate_execution_gas(path);
let gas_price = provider.get_gas_price().await?;
let net_profit = actual_output
.checked_sub(estimate.input)?
.checked_sub(gas_cost * gas_price)?;
Ok(VerifiedProfit {
gross: actual_output - estimate.input,
gas_cost: gas_cost * gas_price,
net: net_profit,
is_profitable: net_profit > MIN_PROFIT_THRESHOLD,
})
}
Only opportunities that pass both layers are sent to the executor.
Flash Loan Integration
ChainFusion supports flash loans from three providers, selected based on availability and fee structure:
- Uniswap V3/V4 Flash Swaps: Zero fee (you just need to repay within the same transaction)
- Aave V3: 0.09% fee (cheapest dedicated flash loan)
- Balancer: Zero fee flash loans
Flash loans are the key innovation that makes this system capital-efficient. Instead of needing $100K+ in starting capital, the system borrows the entire trade amount, executes the triangle, repays the loan, and keeps the profit — all atomically in one transaction:
pub fn select_flash_provider(
base_token: &Address,
amount: U256,
) -> FlashProvider {
// Prefer zero-fee providers
if uniswap_v3_supports(base_token, amount) {
FlashProvider::UniswapV3
} else if balancer_supports(base_token, amount) {
FlashProvider::Balancer
} else {
FlashProvider::AaveV3 // 0.09% fee fallback
}
}
The Smart Contract: FlashArbitrage.sol
The on-chain component is a Solidity contract that receives the flash loan, executes the three swaps, repays the loan, and sends profit to the operator:
contract FlashArbitrage is IUniswapV3FlashCallback, IBalancerFlashLoanRecipient {
address public immutable owner;
function executeArbitrage(
FlashParams calldata params
) external onlyOwner {
// Initiate flash loan based on selected provider
if (params.provider == Provider.UniswapV3) {
IUniswapV3Pool(params.flashPool).flash(
address(this), params.amount0, params.amount1, abi.encode(params)
);
} else if (params.provider == Provider.Balancer) {
IBalancerVault(BALANCER_VAULT).flashLoan(
this, params.tokens, params.amounts, params.userData
);
}
}
function uniswapV3FlashCallback(
uint256 fee0, uint256 fee1, bytes calldata data
) external override {
FlashParams memory params = abi.decode(data, (FlashParams));
uint256 amountOut = params.borrowAmount;
// Execute triangle: Leg 1 → Leg 2 → Leg 3
for (uint i = 0; i < 3; i++) {
amountOut = _executeLeg(params.legs[i], amountOut);
}
// Repay flash loan
uint256 amountOwed = params.borrowAmount + fee0;
require(amountOut > amountOwed, "Not profitable");
IERC20(params.baseToken).transfer(msg.sender, amountOwed);
// Send profit to owner
uint256 profit = amountOut - amountOwed;
IERC20(params.baseToken).transfer(owner, profit);
}
function _executeLeg(Leg memory leg, uint256 amountIn) internal returns (uint256) {
IERC20(leg.tokenIn).approve(leg.router, amountIn);
if (leg.dexType == DexType.UniswapV3) {
return _swapV3(leg, amountIn);
} else if (leg.dexType == DexType.UniswapV2) {
return _swapV2(leg, amountIn);
} else {
return _swapCurve(leg, amountIn);
}
}
}
The contract is designed to be minimal and gas-efficient. Each _executeLeg function calls the appropriate DEX router directly, avoiding unnecessary intermediate steps.
MEV Protection: Flashbots Integration
Submitting arbitrage transactions to the public mempool is suicide — MEV bots will front-run you or sandwich your trade. ChainFusion uses Flashbots to submit transactions as private bundles that bypass the public mempool entirely:
pub struct FlashbotsExecutor {
signer: LocalWallet,
flashbots_relay: Url,
provider: Arc<Provider<Http>>,
}
impl FlashbotsExecutor {
pub async fn submit_bundle(
&self,
tx: TypedTransaction,
target_block: u64,
) -> Result<BundleResult> {
let signed_tx = self.signer.sign_transaction(&tx).await?;
let bundle = FlashbotsBundle {
txs: vec![signed_tx],
block_number: target_block,
min_timestamp: None,
max_timestamp: Some(Utc::now().timestamp() as u64 + 120),
};
let response = self.send_bundle(&bundle).await?;
match response.status {
BundleStatus::Included => Ok(BundleResult::Success(response.tx_hash)),
BundleStatus::BlockPassedWithoutInclusion => Ok(BundleResult::Missed),
BundleStatus::AccountNonceTooHigh => Err(LighterError::NonceMismatch),
}
}
}
Flashbots bundles are sent directly to block builders, meaning your transaction is either included atomically or not at all — no partial execution, no front-running, no wasted gas on reverts.
Multi-Chain Support
ChainFusion is designed to run on any EVM-compatible chain. The configuration system allows adding new chains with just a config file:
pub struct ChainConfig {
pub chain_id: u64,
pub name: String,
pub rpc_ws: String,
pub rpc_http: String,
pub flashbots_relay: Option<String>,
pub contract_address: Address,
pub pools: Vec<PoolConfig>,
pub min_profit_wei: U256,
pub gas_multiplier: f64,
}
Supported chains include Ethereum, BSC, Polygon, Arbitrum, Base, Optimism, and Solana (via a separate adapter that translates the AMM model to Solana's program architecture).
REST API & Backtesting
ChainFusion includes a REST API for managing strategies, monitoring performance, and adjusting parameters at runtime:
GET /api/v1/status - System health & active paths
GET /api/v1/opportunities - Current detected opportunities
GET /api/v1/trades - Historical trade log
POST /api/v1/config/paths - Enable/disable specific paths
POST /api/v1/config/params - Adjust min profit, gas limits
GET /api/v1/metrics - Prometheus-compatible metrics
The backtesting engine replays historical pool events to evaluate strategy performance. It uses the same profit calculation and path evaluation code as the live system, ensuring backtest results closely match production behavior:
pub async fn run_backtest(
config: &BacktestConfig,
historical_events: Vec<PoolEvent>,
) -> BacktestResult {
let mut engine = ArbitrageEngine::new(config.paths.clone());
let mut total_profit = U256::zero();
let mut trade_count = 0;
for event in historical_events {
engine.update_pool(&event);
if let Some(opportunity) = engine.scan_paths() {
if opportunity.net_profit > config.min_profit {
total_profit += opportunity.net_profit;
trade_count += 1;
}
}
}
BacktestResult { total_profit, trade_count, period: config.period }
}
Conclusion
Building ChainFusion taught me that successful arbitrage systems are won on engineering, not on strategy. The triangular arbitrage math is straightforward — the hard part is executing it faster and more reliably than everyone else. Rust's zero-cost abstractions, combined with Tokio's async runtime, give us the performance foundation. Flash loans eliminate capital requirements. Flashbots eliminate MEV risk. And the dual-layer profit verification prevents costly false positives.
The system currently monitors 14 pools across 26 paths, but the architecture is designed to scale horizontally — add more pools, more chains, more paths, and the workspace structure keeps the codebase manageable.
DeFi arbitrage is an adversarial game. The edge comes from infrastructure, not information. Every microsecond of latency, every unnecessary memory allocation, every wasted gas unit is a competitive disadvantage. Rust is the right tool for this job.
Explore the full source at github.com/robustfengbin/ChainFusion-Arbitrage.