nautechsystems/nautilus_trader · error

Token {} reports unexpected decimals

Error message

Token {} reports unexpected decimals

What it means

Raised during pre-trade verification when an ERC20 token's on-chain decimals() disagrees with the token.decimals value recorded in the plan/manifest. Decimal mismatch would corrupt amount scaling (raw vs human units), so the library refuses to proceed. The check runs per swap token in the verification loop.

Source

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

    ));

    for token in [&plan.pool.token0, &plan.pool.token1] {
        let decimals_call = ERC20::decimalsCall.abi_encode();
        let decimals = required_verification(
            executor
                .verification
                .verify_decoded_call(
                    None,
                    &token.address,
                    U256::ZERO,
                    &decimals_call,
                    block,
                    |result| ERC20::decimalsCall::abi_decode_returns(result).map_err(Into::into),
                )
                .await,
            "swap token decimals",
        )?;
        anyhow::ensure!(
            decimals.value == token.decimals,
            "Token {} reports unexpected decimals",
            token.address,
        );
        decisions.push(verification_decision(&decimals, Some(block), Some(block)));
    }

    let allowance_call = ERC20::allowanceCall {
        owner: executor.wallet_address,
        spender: plan.router,
    }
    .abi_encode();
    let allowance = required_verification(
        executor
            .verification
            .verify_decoded_call(
                None,
                &plan.token_in,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Correct token.decimals in the deployment manifest to match the on-chain decimals() of token.address
  2. Re-derive the manifest from on-chain reads (decimals, symbol, address) instead of manual entry
  3. Verify token.address is the intended contract for the target chain (address typos cause cross-token reads)
  4. If a token genuinely changed decimals, rebuild the plan and re-check all amount conversions

Example fix

// before
"token": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 18 } // USDC copied as 18
// after
"token": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6 }
Defensive patterns

Strategy: validation

Validate before calling

async fn check_token_decimals(token: Address, manifest_decimals: u8) -> Result<(), String> {
    let actual: u8 = erc20_decimals(token).await?;
    if actual != manifest_decimals { return Err(format!("token {token} decimals {actual} != manifest {manifest_decimals}")); }
    Ok(())
}

Type guard

fn decimals_match(manifest_decimals: u8, actual: u8) -> bool { manifest_decimals == actual }

Prevention

When it happens

Trigger: Executing a swap plan for token.address where ERC20.decimals() != token.decimals — e.g. manifest recorded 18 for USDC (6) or the token contract changed decimals via upgrade/rebase mechanics.

Common situations: Hand-written deployment manifest with copied decimals from the wrong token, plan reused across chains where the same symbol maps to different contracts, fee-on-transfer/upgradable tokens that changed decimals, or address typo pulling a different token.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


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