nautechsystems/nautilus_trader · error · anyhow::Error

Ticks {tick_lower} and {tick_upper} must be multiples of the

Error message

Ticks {tick_lower} and {tick_upper} must be multiples of the tick spacing

What it means

This error is thrown by `validate_ticks` in the liquidity profiler when either the lower or upper tick of a position is not an integer multiple of the pool's tick spacing. Concentrated-liquidity pools (e.g. Uniswap v3) only accept positions whose tick boundaries align to the pool's configured tick spacing, so the profiler rejects any mint/burn with misaligned ticks before analytics are updated.

Source

Thrown at crates/model/src/defi/pool_analysis/profiler.rs:1628

    ///
    /// Ensures ticks are properly ordered, aligned to tick spacing, and within
    /// valid bounds. Used by all position-related operations.
    ///
    /// # Errors
    ///
    /// This function returns an error if:
    /// - `tick_lower >= tick_upper` (invalid range).
    /// - Ticks are not multiples of pool's tick spacing.
    /// - Ticks are outside `MIN_TICK/MAX_TICK` bounds.
    fn validate_ticks(&self, tick_lower: i32, tick_upper: i32) -> anyhow::Result<()> {
        if tick_lower >= tick_upper {
            anyhow::bail!("Invalid tick range: {tick_lower} >= {tick_upper}")
        }

        if tick_lower % self.pool.tick_spacing.unwrap() as i32 != 0
            || tick_upper % self.pool.tick_spacing.unwrap() as i32 != 0
        {
            anyhow::bail!(
                "Ticks {tick_lower} and {tick_upper} must be multiples of the tick spacing"
            )
        }

        if tick_lower < PoolTick::MIN_TICK || tick_upper > PoolTick::MAX_TICK {
            anyhow::bail!("Invalid tick bounds for {tick_lower} and {tick_upper}");
        }
        Ok(())
    }

    /// Updates all liquidity analytics.
    fn update_liquidity_analytics(&mut self) {
        self.analytics.liquidity_utilization_rate = self.liquidity_utilization_rate();
    }

    /// Returns the pool's active liquidity tracked by the tick map.
    ///
    /// This represents the effective liquidity available for trading at the current price.

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Round tick_lower/tick_upper to the pool's tick spacing (floor toward MIN_TICK for lower, ceiling toward MAX_TICK for upper) before calling mint/burn handlers.
  2. Fetch the correct `tick_spacing` for the specific pool rather than assuming a value; ensure `pool.tick_spacing` is populated (it is unwrapped here).
  3. Use existing tick-math helpers (e.g. nearest usable tick functions) to derive ticks from a price range.
  4. Log the offending ticks and spacing to confirm alignment before retrying the event processing.

Example fix

// before
let (tick_lower, tick_upper) = (lower_from_price, upper_from_price);
profiler.execute_mint(owner, tick_lower, tick_upper, amount);
// after
let spacing = pool.tick_spacing.unwrap() as i32;
let tick_lower = (lower_from_price / spacing) * spacing;
let tick_upper = ((upper_from_price + spacing - 1) / spacing) * spacing;
profiler.execute_mint(owner, tick_lower, tick_upper, amount);
Defensive patterns

Strategy: validation

Validate before calling

fn ticks_aligned(tick_lower: i32, tick_upper: i32, tick_spacing: i32) -> bool {
    tick_spacing > 0
        && tick_lower % tick_spacing == 0
        && tick_upper % tick_spacing == 0
        && tick_lower < tick_upper
}

Try / catch

match profiler.execute_mint(owner, tick_lower, tick_upper, amount) {
    Ok(_) => {},
    Err(e) if e.to_string().contains("must be multiples of the tick spacing") => {
        // recompute ticks rounded to spacing and retry
    },
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `process_mint`, `execute_mint`, `process_burn`, or `execute_burn` with `tick_lower` or `tick_upper` where `tick % tick_spacing != 0` (tick_spacing read from `self.pool.tick_spacing`, which is also unwrapped and would panic if None). The ticks may individually be valid but still misaligned, e.g. lower=10, upper=210 with tick_spacing 60.

Common situations: Parsing on-chain Mint/Burn events from pools with non-standard tick spacing; hardcoding tick ranges copied from a pool with a different fee tier/spacing; computing ticks via arithmetic that doesn't round to spacing (e.g. price-to-tick conversion without rounding down to the nearest valid tick).

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/653e3f5d7ee4877e. Report an issue: GitHub.