nautechsystems/nautilus_trader · error · anyhow::Error

Invalid tick range: {tick_lower} >= {tick_upper}

Error message

Invalid tick range: {tick_lower} >= {tick_upper}

What it means

validate_ticks enforces Uniswap-v3 position constraints: tick_lower must be strictly less than tick_upper, both must be multiples of the pool's tick spacing, and both within MIN_TICK/MAX_TICK. It bails immediately when tick_lower >= tick_upper since such a range defines no valid position. Used by both mint and burn processing.

Source

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

        // Safe to cast to u64: Since active_liquidity <= total_liquidity,
        // the ratio is guaranteed to be <= PRECISION (1_000_000), which fits in u64
        ratio.to::<u64>() as f64 / f64::from(PRECISION)
    }

    /// Validates tick range for position operations.
    ///
    /// 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) {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Swap or sort the tick arguments so tick_lower < tick_upper before calling
  2. Validate the tick range (and spacing alignment) at your call site before invoking mint/burn
  3. Check the event decoder: ticks should be i32 read from the correct log fields
  4. Skip mint/burn events with invalid tick ranges as data errors, logging them for review

Example fix

// before
profiler.process_mint(owner, tick_upper, tick_lower, amount)?;
// after
let (lower, upper) = (tick_lower.min(tick_upper), tick_lower.max(tick_upper));
profiler.process_mint(owner, lower, upper, amount)?;
Defensive patterns

Strategy: validation

Validate before calling

anyhow::ensure!(tick_lower < tick_upper, "invalid tick range");
anyhow::ensure!(tick_lower % spacing == 0 && tick_upper % spacing == 0, "ticks off spacing");

Type guard

fn valid_tick_range(lower: i32, upper: i32, spacing: i32) -> bool {
    lower < upper && lower % spacing == 0 && upper % spacing == 0
}

Try / catch

match profiler.process_mint(owner, lower, upper, amount) {
    Err(e) if e.to_string().contains("Invalid tick range") => { /* skip malformed event */ }
    other => other?,
}

Prevention

When it happens

Trigger: Calling process_mint/process_burn/execute_mint/execute_burn with tick_lower >= tick_upper — e.g. swapped arguments, decoded tick values from malformed events, or burns on positions that were never validated.

Common situations: Argument order mistakes when calling the API; event decoding bugs producing negative/garbage ticks; MIN_TICK==MAX_TICK style sentinel misuse; ticks decoded as unsigned then truncated.

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/9a9c11d00b9ddb4a. Report an issue: GitHub.