nautechsystems/nautilus_trader · error
Restored pool has no fee
Error message
Restored pool {instrument_id} has no fee What it means
restore_swap_plan needs the Uniswap V3 fee tier (U24) of the restored pool to build the swap parameters. If the resolved pool has no fee value (pool.fee is None), the swap plan cannot be constructed and reconciliation aborts. The library throws this because a Uniswap V3 swap cannot be encoded without a fee tier.
Solutions
- Fix the instrument/pool registration so the Uniswap V3 fee tier is populated (re-register the pool or enable fee discovery) and restart.
- Verify the instrument provider config points at the correct factory/pool so fee metadata is fetched.
- Restore a complete cache snapshot that includes the pool's fee metadata.
- As a last resort, resolve the pending intent manually in the durable store, since the swap cannot be rebuilt from a fee-less pool.
Defensive patterns
Strategy: validation
Validate before calling
let pool = client.resolve_pool(&instrument_id)?;
if pool.fee.is_none() {
eprintln!("pool for {instrument_id} has no fee tier; fix instrument registration before connecting");
} Type guard
fn has_fee(pool: &Pool) -> bool {
pool.fee.is_some()
} Prevention
- Enable fee discovery or explicitly set the Uniswap V3 fee tier when registering pools
- Validate instrument definitions (fee present) at startup before live trading
- Restore complete cache snapshots including pool metadata
- Test reconciliation against a copy of the durable store after any instrument-provider change
When it happens
Trigger: reconcile_unresolved_execution or build_execution_verification_migration calls restore_swap_plan; resolve_pool(&instrument_id) returns a pool whose fee field is None. Happens when the pool definition for the instrument is incomplete in the instrument provider/cache, e.g. fee not queried/set at registration or a partially-defined instrument restored from cache.
Common situations: Instrument provider configured without fee discovery (fee never populated for that pool); custom/edited instrument definitions missing the fee attribute; adapter version where fee was optional combined with a pool source that omits it; cache restored from an incomplete snapshot.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- Persisted swap intent has no input amount
- Persisted swap intent has no pool address
- Contract address should be set in logs
- Contract address should be set in logs
- Finalized transaction
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/4b7b0b1676d761a3.
Report an issue: GitHub.
Appendix: source
Thrown at crates/adapters/blockchain/src/execution/client.rs:1082
intent
.pool_address
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no pool address"))?,
)?;
anyhow::ensure!(
pool.address == pool_address,
"Persisted pool {pool_address} does not match restored pool {}",
pool.address
);
let amount_in = U256::from_str(
intent
.amount_in
.as_deref()
.ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no input amount"))?,
)?;
let fee = U24::try_from(
pool.fee
.ok_or_else(|| anyhow::anyhow!("Restored pool {instrument_id} has no fee"))?,
)?;
let quote_token = pool.get_quote_token();
let quote_currency = Currency::new_checked(
"e_token.symbol,
quote_token.decimals,
0,
"e_token.name,
CurrencyType::Crypto,
)?;
let (token_in, token_out) = swap_token_pair(
order.order_side(),
pool.get_base_token().address,
quote_token.address,
)?;
let factory = self.uniswap_v3_factory()?;
anyhow::ensure!(
pool.dex.factory == factory,
"Restored pool {instrument_id} references factory {}, expected registered factory {factory}",View on GitHub (pinned to 18893faf8b)