{"record":{"id":"1cffea0dd520a76d","repo":"nautechsystems/nautilus_trader","slug":"swap-quote-output-amount-amount-is-not-a-positiv","errorCode":null,"errorMessage":"Swap quote output amount {amount} is not a positive output","messagePattern":"Swap quote output amount (.+?) is not a positive output","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/execution/client.rs","lineNumber":2480,"sourceCode":"        if !(raw % divisor).is_zero() {\n            anyhow::bail!(\n                \"Order quantity {quantity} is not exactly representable in {decimals} base token decimals\"\n            );\n        }\n        Ok(raw / divisor)\n    }\n}\n\n/// Extracts the positive output amount from an exact-input swap quote.\nfn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {\n    let amount = if zero_for_one {\n        quote.amount1\n    } else {\n        quote.amount0\n    };\n\n    if !amount.is_negative() {\n        anyhow::bail!(\"Swap quote output amount {amount} is not a positive output\");\n    }\n    Ok(amount.unsigned_abs())\n}\n\n/// Derives the minimum output accepted for the swap: the quoted output reduced by\n/// `slippage_bps`, in exact integer arithmetic. Rejects a zero minimum, which would leave\n/// the swap without slippage protection.\nfn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {\n    if slippage_bps >= BPS_DENOMINATOR {\n        anyhow::bail!(\"Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}\");\n    }\n    let min_amount_out = quoted_amount_out\n        .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))\n        .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))\n        .ok_or_else(|| anyhow::anyhow!(\"Minimum output derivation overflow\"))?;\n    if min_amount_out.is_zero() {\n        anyhow::bail!(\n            \"Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage\"","sourceCodeStart":2462,"sourceCodeEnd":2498,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/2114cf6f761429e0adb5ca9596fcd7b895b16011/crates/adapters/blockchain/src/execution/client.rs#L2462-L2498","documentation":"exact_output_amount extracts the output leg from a SwapQuote. Quote amounts are signed pool deltas (amount0/amount1 as I256, negative = tokens paid out to the trader, positive = tokens taken in), so the trader's output leg MUST be a negative delta. The bail fires when the selected amount is zero or positive, i.e. the quote says that token is not actually being received.","triggerScenarios":"zero_for_one is computed inconsistently with the pool's token0/token1 ordering, so the wrong leg (the input leg) is read as the output; a degenerate or zero-output quote (e.g. amount_in too large for current reserves producing a zero/negative-direction result); passing a stale quote whose direction flags no longer match the pool state.","commonSituations":"Sorting token addresses differently than the pool definition when deriving zero_for_one; reusing quote structs built against a different pool orientation; edge tests with synthetic quotes that fill amount0/amount1 as unsigned values.","solutions":["Recompute zero_for_one from the pool's token0/token1 ordering (token_in == token0 means zero_for_one = true) rather than from insertion order of the plan","Re-request a fresh quote and check both signed legs before submitting","If quotes are built in your own code, populate the output leg as a negative pool delta (e.g. -out_amount as I256)"],"exampleFix":"// before: direction assumed from plan field order\nlet zero_for_one = plan.token_in < plan.token_out; // wrong ordering basis\n\n// after: derive from the pool's token0 ordering\nlet (token0, _) = pool.token0_token1();\nlet zero_for_one = plan.token_in == token0;","handlingStrategy":"validation","validationCode":"// Validate the quote's output leg before execution\nlet (token0, _token1) = pool.token0_token1();\nlet zero_for_one = plan.token_in == token0;\nlet out_leg = if zero_for_one { quote.amount1 } else { quote.amount0 };\nanyhow::ensure!(out_leg.is_negative(), \"quote has no output leg; direction mismatch\");","typeGuard":"fn quote_has_output(quote: &SwapQuote, zero_for_one: bool) -> bool {\n    let amount = if zero_for_one { quote.amount1 } else { quote.amount0 };\n    amount.is_negative()\n}","tryCatchPattern":"// Direction/quote bugs are deterministic: catch, re-derive zero_for_one from token0\n// ordering, re-quote, and retry once; never resubmit the same stale quote.\nmatch execute_swap(&plan, &quote).await {\n    Err(e) if e.to_string().contains(\"not a positive output\") => {\n        let quote = requote(&pool, &plan).await?;\n        execute_swap(&plan, &quote).await\n    }\n    other => other,\n}","preventionTips":["Derive zero_for_one only from the pool's token0/token1 ordering","Simulate quotes with a known small swap in tests and assert the output leg sign","Discard quotes older than a few blocks and re-quote before submission"],"tags":["swap","quote","uniswap","direction","signed-amounts"],"backgroundTag":"invalid-swap-quote-output","analyzedSha":"2114cf6f761429e0adb5ca9596fcd7b895b16011","analyzedAt":"2026-08-21T11:28:30.864Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}