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:
In terms of tracked state:
sumX = k√n
For a consolidated pool with multiple ticks, the condition is:
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_outWhy Binary Search Off-Chain?
Computing which ticks will cross requires:
- Starting from the current state, simulate the trade
- Check if the crossing condition is met
- If yes, flip the tick and continue with remaining input
- 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 highRe-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:
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_CROSSINGSIn 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'sgetSwapQuotefunction returns the trade recipe. Pass it directly toswap_with_crossings.