The routing problem
When IVZA processes a swap intent -- "swap 500 USDC to SOL" -- it needs to decide how that swap gets executed. Which liquidity pool? If there are multiple pools for the same pair, which one gives the best output? Should the swap go through an intermediate token? Should it be split across multiple pools?
This is a constrained optimization problem. The solver's job is to find the execution path that maximizes output while respecting slippage limits, pool depth constraints, and compute budget.
Pool graph
The solver maintains a PoolGraph -- a weighted graph where nodes are token mints and edges are liquidity pools. Each edge carries the pool's address, reserves, fee rate, and pool type (constant-product AMM, concentrated liquidity, or orderbook).
pub struct PoolInfo {
pub address: Pubkey,
pub token_a: Pubkey,
pub token_b: Pubkey,
pub reserve_a: u64,
pub reserve_b: u64,
pub fee_bps: u16,
pub pool_type: PoolType,
pub active: bool,
}
pub enum PoolType {
ConstantProduct, // x * y = k
ConcentratedLiquidity { tick_spacing: i32 },
Orderbook,
}The PoolRegistry is backed by DashMap for thread-safe concurrent access. Pools can be registered, updated, and deactivated at runtime without locking the entire registry.
Dijkstra routing
Finding the best route from token A to token B is a shortest-path problem on the pool graph. The RouteEngine uses Dijkstra's algorithm with a cost function that accounts for:
- Output ratio -- How much output you get per unit of input, accounting for fees and price impact
- Hop penalty -- Each additional hop reduces the score. More hops means more transactions, more compute, more slippage
- Price impact -- Large swaps relative to pool depth get penalized. A $500 swap on a $10M pool has negligible impact. The same swap on a $5K pool is catastrophic
- Pool depth -- Deeper pools score higher at equal output ratios. More liquidity means more reliable execution
fn score_route(&self, route: &Route, amount: u64) -> f64 {
let output_ratio =
route.estimated_output as f64 / amount as f64;
let hop_penalty =
0.98_f64.powi(route.hops.len() as i32 - 1);
let impact_factor =
1.0 - route.total_price_impact;
let depth_factor = (route.min_depth as f64)
.ln()
.max(1.0)
/ 25.0;
output_ratio * hop_penalty * impact_factor * depth_factor
}The router supports up to 4 hops (A → B → C → D → E) and prunes routes that cycle through the same token. For each token pair, it returns the top-K routes sorted by score.
Constant-product math
For standard AMM pools, the output calculation follows the constant-product formula. Given reserves (x, y) and input amount dx (after fees):
pub fn calculate_output(
reserve_in: u64,
reserve_out: u64,
amount_in: u64,
fee_bps: u16,
) -> u64 {
let fee_adjusted = amount_in as u128
* (10_000 - fee_bps as u128)
/ 10_000;
let numerator = fee_adjusted * reserve_out as u128;
let denominator = reserve_in as u128 + fee_adjusted;
(numerator / denominator) as u64
}
pub fn calculate_price_impact(
reserve_in: u64,
amount_in: u64,
) -> f64 {
amount_in as f64 / (reserve_in as f64 + amount_in as f64)
}For concentrated liquidity pools, the math is more involved. The output depends on which tick ranges the swap traverses, and the available liquidity at each tick. IVZA's CLMM implementation walks the tick array, computing partial outputs at each range until the input amount is fully consumed.
Greedy solver
The GreedySolver is the fast path. For each swap in the execution plan, it independently finds the best route and assigns it. No coordination between swaps. Simple and fast.
This works well when swaps are independent -- different token pairs, different pools. It breaks down when multiple swaps compete for the same liquidity. If two swaps both route through the same pool, the first one changes the reserves, making the second one's estimated output wrong.
impl Solver for GreedySolver {
fn solve(
&self,
swaps: &[SwapRequest],
router: &RouteEngine,
) -> Result<SolverResult> {
let mut results = Vec::new();
for swap in swaps {
let route = router.find_best_route(
&swap.input_mint,
&swap.output_mint,
swap.amount,
)?;
results.push(SolvedSwap {
request: swap.clone(),
route,
});
}
Ok(SolverResult { swaps: results })
}
}Branch-and-bound solver
The BranchAndBoundSolver is the optimizer. It considers all swaps together, exploring different route combinations to find the globally optimal assignment.
The algorithm:
- Baseline -- Start with the greedy solution as the initial upper bound
- Variable ordering -- Sort swaps by difficulty. Swaps with fewer viable routes are assigned first (fail-first heuristic). This prunes the search tree earlier
- Branching -- For each swap, try each viable route. After assigning a route, update the virtual reserves of affected pools
- Bounding -- At each node, compute an optimistic bound for the remaining unassigned swaps. If the bound can't beat the current best, prune the branch
- Timeout -- Hard timeout (default 100ms). If reached, return the best solution found so far
fn search(
&self,
depth: usize,
assigned: &mut Vec<SolvedSwap>,
virtual_reserves: &mut HashMap<Pubkey, (u64, u64)>,
best: &mut f64,
best_solution: &mut Vec<SolvedSwap>,
deadline: Instant,
) {
if Instant::now() > deadline { return; }
if depth == self.swaps.len() {
let score = total_output(assigned);
if score > *best {
*best = score;
*best_solution = assigned.clone();
}
return;
}
let swap = &self.swaps[depth];
for route in &self.routes[depth] {
// Check if route is still viable with
// current virtual reserves
let output = requote(&route, virtual_reserves);
let bound = output + optimistic_remaining(
depth + 1, &self.swaps, virtual_reserves
);
if bound <= *best { continue; } // prune
// Branch: assign this route
apply_reserves(virtual_reserves, &route);
assigned.push(SolvedSwap { route, output });
self.search(
depth + 1, assigned,
virtual_reserves, best,
best_solution, deadline
);
// Backtrack
assigned.pop();
revert_reserves(virtual_reserves, &route);
}
}When to use which
The SolverEngine defaults to greedy and switches to branch-and-bound when the optimization opportunity is high:
- 1-2 swaps, no shared pools -- Greedy. Branch-and-bound won't find anything better.
- 3+ swaps, shared pools -- Branch-and-bound. The reserve interaction effects make greedy suboptimal.
- High-value swaps -- Branch-and-bound. The savings from optimal routing justify the compute cost.
In benchmarks, branch-and-bound finds routes with 2-8% better output than greedy for graphs with 5+ swaps sharing liquidity pools. For simple operations, the results are identical and greedy runs 10x faster.
Post-solve optimization
After the solver assigns routes, the ExecutionOptimizer applies a final pass:
- Split detection -- If a swap is large relative to pool depth (impact > 1%), split it across multiple pools using gradient descent to find the optimal split ratio
- Lane reordering -- Reorder transactions within each lane to maximize the probability of successful execution (higher-value swaps first)
- Merge compatible transactions -- If two small swaps in the same lane use the same pool, merge them into a single instruction to save compute
The optimizer reports savings in basis points. For typical DeFi operations, the combined solver + optimizer pipeline produces 3-15 bps improvement over naive sequential execution.