Tick Crossing

Tick crossing is the most complex operation in Orbital AMM. When a trade moves reserves across a tick boundary, the pool's consolidated state must be recomputed. This page explains the crossing condition and the trade recipe format.

The Crossing Condition

A tick is at the boundary when:

x · v = k

In terms of tracked state:

sumX / √n = k
sumX = k√n

For a consolidated pool with multiple ticks, the condition is:

α_int = k_bound
sumX / (r_int√n) = k_bound / (r_bound√n)
sumX · r_bound = k_bound · r_int

This cross-multiplied form avoids division and is what the contract checks.

Trade Recipe Format

The SDK computes the full trade path off-chain and submits it as a recipe:

interface TradeSegment {
  amountIn: bigint;
  amountOut: bigint;
  tickCrossedId: number;  // 0 if no crossing
  newTickState: number;   // 0=INTERIOR, 1=BOUNDARY
}

// Example: swap that crosses 2 ticks
const recipe: TradeSegment[] = [
  {
    amountIn: 50_000_000n,
    amountOut: 49_876_543n,
    tickCrossedId: 0,  // No crossing in first segment
    newTickState: 0
  },
  {
    amountIn: 50_000_000n,
    amountOut: 49_654_321n,
    tickCrossedId: 42,  // Crossing tick 42
    newTickState: 1     // Tick becomes BOUNDARY
  },
  {
    amountIn: 0n,
    amountOut: 12_345_678n,
    tickCrossedId: 17,  // Crossing tick 17
    newTickState: 0     // Tick becomes INTERIOR
  }
];

Verification Loop

The contract verifies each segment sequentially:

for segment in trade_recipe:
    # 1. Apply segment's input/output
    sumX = sumX + segment.amountIn - segment.amountOut
    sumXSq = update_sumXSq(sumXSq, segment, reserves)

    # 2. Verify invariant holds after this segment
    lhs = r_int ** 2
    rhs = compute_rhs(sumX, sumXSq, r_int, s_bound)
    assert abs(lhs - rhs) <= TOLERANCE

    # 3. If crossing claimed, verify and flip tick
    if segment.tickCrossedId != 0:
        tick = load_tick(segment.tickCrossedId)

        # Verify crossing condition
        assert sumX * tick.r == tick.k * r_int

        # Flip tick state
        old_state = tick.state
        tick.state = segment.newTickState

        # Re-consolidate
        if old_state == INTERIOR:
            r_int -= tick.r
            s_bound += tick.effective_radius
        else:
            s_bound -= tick.effective_radius
            r_int += tick.r

        # Update k_bound if this tick is now outermost
        if segment.newTickState == BOUNDARY:
            k_bound = max(k_bound, tick.k)

        save_tick(tick)

# Final check: total output >= minOut
assert total_output >= min_out

Why Binary Search Off-Chain?

Computing which ticks will cross requires:

  1. Starting from the current state, simulate the trade
  2. Check if the crossing condition is met
  3. If yes, flip the tick and continue with remaining input
  4. Repeat until all input is consumed

This is inherently iterative — the number of iterations depends on the trade size and tick configuration. On-chain, this would be unbounded gas.

Off-chain, the SDK can use binary search to efficiently find crossing points:

# SDK: Find how much input is needed to reach tick boundary
def find_crossing_input(pool_state, tick) -> float:
    # Current α_int
    alpha = pool_state.sumX / sqrt(pool_state.n)

    # Distance to boundary
    delta_alpha = tick.k - alpha

    # Input needed (approximate, ignores curvature)
    return delta_alpha * sqrt(pool_state.n)

# Binary search for exact amount
low, high = 0, total_input
while high - low > epsilon:
    mid = (low + high) / 2
    simulated = simulate_trade(pool_state, mid)
    if crossed_boundary(simulated, tick):
        high = mid
    else:
        low = mid
return high

Re-Consolidation After Crossing

When a tick crosses, the consolidated state changes:

  • INTERIOR → BOUNDARY: Remove r from r_int, add effective_radius to s_bound
  • BOUNDARY → INTERIOR: Remove effective_radius from s_bound, add r to r_int

The effective_radius of a boundary tick is:

s_eff = √(r² − (k − r√n)²)

This is computed once when the tick is added and cached in the tick box.

Max Crossings Limit

To prevent DoS via complex trade recipes, the contract enforces:

MAX_TICK_CROSSINGS = 20

assert len(trade_recipe) <= MAX_TICK_CROSSINGS

In practice, most swaps cross 0-2 ticks. The limit is only hit for very large trades that exhaust multiple liquidity zones.

Implementation note: The SDK's getSwapQuotefunction returns the trade recipe. Pass it directly to swap_with_crossings.