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
- Correct token.decimals in the deployment manifest to match the on-chain decimals() of token.address
- Re-derive the manifest from on-chain reads (decimals, symbol, address) instead of manual entry
- Verify token.address is the intended contract for the target chain (address typos cause cross-token reads)
- 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
- Read decimals on-chain when generating manifests; never copy from token lists blindly
- Validate token.address against the official registry for the target chain
- Re-verify decimals for upgradable tokens before each plan build
- Keep amount conversions (raw <-> human) centralized so a decimals fix propagates everywhere
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
- Router allowance after transaction {} does not equal the req
- DEX {dex_id} is not registered in the data client
- `router_addresses` must contain at least one router address
- Quote spend limit for {token_in} -> {token_out} is denominat
- Router {router} is not in the configured `router_addresses`
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/8aebdd3b56cac754.
Report an issue: GitHub.