nautechsystems/nautilus_trader · error · anyhow::Error

Finalized Swap side {} does not match Sell order

Error message

Finalized Swap side {} does not match Sell order

What it means

Side check in emit_finalized_swap_fill: the executor only sells the base token for quote, so the order is a Sell and the calculated trade.side derived from the Swap event must equal OrderSide::Sell. The mismatch means the decoded swap moved the tokens in the opposite direction to what the order implies.

Source

Thrown at crates/adapters/blockchain/src/execution/client.rs:2281

        included.receipt.block_hash,
        block.hash
    );
    let timestamp_ns = block
        .timestamp
        .checked_mul(NANOSECONDS_IN_SECOND)
        .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
    let mut swap = event.to_pool_swap(
        plan.pool.chain.clone(),
        plan.instrument_id,
        plan.pool.pool_identifier,
        UnixNanos::from(timestamp_ns),
    );
    swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
    let trade = swap
        .trade_info
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("Finalized Swap has no calculated trade information"))?;
    anyhow::ensure!(
        trade.order_side == OrderSide::Sell,
        "Finalized Swap side {} does not match Sell order",
        trade.order_side
    );
    let gas_cost = included
        .receipt
        .effective_gas_price
        .checked_mul(U256::from(included.receipt.gas_used))
        .ok_or_else(|| anyhow::anyhow!("Finalized transaction gas commission overflow"))?;
    let commission = Money::from_u256(gas_cost, plan.pool.chain.native_currency())?;
    let trade_digest = keccak256(format!("{}:{}", included.tx_hash, swap.log_index));
    let trade_digest = trade_digest.to_string();
    let trade_id = TradeId::new_checked(&trade_digest[2..38])?;

    if plan
        .order
        .trade_ids()
        .iter()

View on GitHub (pinned to d1527c24af)

Solutions

  1. Compare the pool's token0/token1 registration against the on-chain pool contract (slot0/token0/token1 getters).
  2. Confirm the order submitted is a Sell of the base instrument, matching the executor's supported direction.
  3. Re-derive zero_for_one from the current pool ordering before signing.
  4. Re-register the pool with corrected token ordering and let reconciliation reprocess.
Defensive patterns

Strategy: validation

Validate before calling

// before submitting:
let expected_side = OrderSide::Sell; // this executor only sells base
if order.side() != expected_side {
    anyhow::bail!("only Sell orders supported by the swap executor");
}
// verify token ordering against chain
assert_eq!(pool.token0.address, chain_pool.token0().await?);

Type guard

fn order_supported_by_swap_executor(order: &Order) -> bool {
    order.side() == OrderSide::Sell
}

Try / catch

Catch the side mismatch, halt fills for that pool, and re-validate pool token registration before any retry; treat as a data-integrity fault, not a transient error.

Prevention

When it happens

Trigger: Pool registered with token0/token1 reversed relative to on-chain order, making the direction derivation flip; a swap direction (zero_for_one) inverted between quoting and signing; submitting a Buy order through this sell-only execution path.

Common situations: Pool metadata imported with token0/token1 swapped; a strategy configured to buy the base token although the adapter only supports selling base for quote; direction flags computed from a stale pool snapshot.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@d1527c24af (2026-08-21). Data as JSON: /api/errors/4319c60301f99085. Report an issue: GitHub.