nautechsystems/nautilus_trader · error
Liquidity subtraction underflow: x={current}, y={y}, delta={
Error message
Liquidity subtraction underflow: x={current}, y={y}, delta={delta} What it means
liquidity_math_add() panics when the signed delta would drive u128 liquidity below zero (Underflow from try_liquidity_math_add), representing an invalid liquidity removal. The message includes x, the signed delta y, and the internal current/delta values.
Source
Thrown at crates/model/src/defi/tick_map/liquidity_math.rs:67
///
/// # Returns
///
/// The resulting liquidity after applying the delta.
///
/// # Panics
///
/// This function panics if:
/// - Adding positive delta causes overflow.
/// - Subtracting causes underflow.
#[must_use]
pub fn liquidity_math_add(x: u128, y: i128) -> u128 {
match try_liquidity_math_add(x, y) {
Ok(value) => value,
Err(LiquidityMathError::Overflow { current, delta }) => {
panic!("Liquidity addition overflow: x={current}, y={y}, delta={delta}")
}
Err(LiquidityMathError::Underflow { current, delta }) => {
panic!("Liquidity subtraction underflow: x={current}, y={y}, delta={delta}")
}
}
}
/// Derives max liquidity per tick from a given tick spacing.
///
/// # Panics
///
/// Panics if `tick_spacing` is zero.
#[must_use]
pub fn tick_spacing_to_max_liquidity_per_tick(tick_spacing: i32) -> u128 {
assert!(tick_spacing != 0, "Tick spacing must be non-zero");
// Calculate min and max tick aligned to tick spacing
let min_tick = (PoolTick::MIN_TICK / tick_spacing) * tick_spacing;
let max_tick = (PoolTick::MAX_TICK / tick_spacing) * tick_spacing;
// Calculate total number of ticks, cast to i64 to avoid potential overflow in subtractionView on GitHub (pinned to 18893faf8b)
Solutions
- Use try_liquidity_math_add and handle LiquidityMathError::Underflow
- Verify event ordering so removals never precede additions
- Assert the tick's tracked liquidity is >= the removal amount before applying
Example fix
// before
let liquidity = liquidity_math_add(current, -delta); // panics if delta > current
// after
let liquidity = try_liquidity_math_add(current, -delta)
.unwrap_or_else(|e| { /* log/handle underflow */ current }); Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check removal against current liquidity
fn remove_is_safe(x: u128, y: i128) -> bool {
y >= 0 || x.checked_sub(y.unsigned_abs()).is_some()
} Try / catch
let liquidity = try_liquidity_math_add(x, y)
.unwrap_or_else(|e| { warn!("underflow: {e:?}"); x }); Prevention
- Track liquidity net per tick and assert non-negative before removing
- Guarantee event ordering (additions before removals) in your ingestion pipeline
- Use the try_ variant wherever state may be inconsistent
When it happens
Trigger: Calling liquidity_math_add(x, negative_y) where |y| > x; transitively via update_liquidity when removing liquidity that was never added at a tick, or apply_swap_quote with inconsistent state.
Common situations: Double-removing liquidity at a tick; off-by-one in tracking liquidity net deltas; replaying events out of order so a removal arrives before its addition.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- Liquidity addition overflow: x={current}, y={y}, delta={delt
- Native currency not specified for chain {}
- Must have the `chain` field set
- `InstrumentId` not applicable to `Block`
- Invalid factory address for DEX {name} on chain {chain} for
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/eb78cab3cbf044c0.
Report an issue: GitHub.