nautechsystems/nautilus_trader · error

Pool manifest references an unpinned contract

Error message

Pool manifest references an unpinned contract

What it means

This error is thrown while validating a blockchain deployment manifest: every address field of a pool entry (pool address, token0, token1, factory, quote_contract) must correspond to a contract identity already pinned in the manifest's contract_addresses set. The library rejects pools that point at contracts that were not declared and hash-pinned, because an unpinned contract could be swapped for malicious or different bytecode at deploy time.

Source

Thrown at crates/adapters/blockchain/src/rpc/verification.rs:1378

        anyhow::ensure!(
            !token.name.trim().is_empty()
                && !token.symbol.trim().is_empty()
                && matches!(token.asset_role.as_str(), "base" | "quote" | "both"),
            "Token manifest identity or asset role is invalid"
        );
    }

    for pool in &manifest.pools {
        for address in [
            &pool.address,
            &pool.token0,
            &pool.token1,
            &pool.factory,
            &pool.quote_contract,
        ] {
            let address = Address::from_str(address)
                .map_err(|_| anyhow::anyhow!("Pool manifest address is invalid"))?;
            anyhow::ensure!(
                contract_addresses.contains(&address),
                "Pool manifest references an unpinned contract"
            );
        }
        anyhow::ensure!(
            pool.fee != 0 && pool.fee <= 1_000_000,
            "Pool fee is invalid"
        );
    }
    let mut call_edges = HashSet::new();
    for edge in &manifest.call_edges {
        anyhow::ensure!(
            matches!(
                edge.purpose.as_str(),
                "wrap" | "approve" | "swap_sell" | "swap_buy"
            ) && matches!(
                edge.call_type.as_str(),
                "call" | "staticcall" | "delegatecall" | "callcode"

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Add a contract entry (with matching address and pinned code hash) for each address the pool references, especially token0/token1 and the factory
  2. Fix the pool's address fields to match the exact pinned contract addresses (parsed Address comparison, case-insensitive)
  3. If the pool came from another network manifest, regenerate the pool entry for this chain's pinned contracts
  4. Re-run the manifest generator so pools and pinned contracts are produced from one consistent source

Example fix

// before
{"pools": [{"address": "0xabc...", "token0": "0x111...", "token1": "0x222...", "factory": "0x333...", "quote_contract": "0x444..."}]}
// contracts section missing 0x111...
// after
{"contracts": [{"address": "0x111...", "role": "Token", "runtime_code_hash": "0x..."}, ...], "pools": [{"address": "0xabc...", "token0": "0x111...", "token1": "0x222...", "factory": "0x333...", "quote_contract": "0x444..."}]}
Defensive patterns

Strategy: validation

Validate before calling

// Rust: pre-validate pool references against pinned contracts
let pinned: HashSet<Address> = manifest.contracts.iter()
    .filter_map(|c| Address::from_str(&c.address).ok())
    .collect();
for pool in &manifest.pools {
    for field in [&pool.address, &pool.token0, &pool.token1, &pool.factory, &pool.quote_contract] {
        let addr = Address::from_str(field)
            .map_err(|_| format!("pool address invalid: {field}"))?;
        anyhow::ensure!(pinned.contains(&addr), "pool references unpinned contract {field}");
    }
}

Type guard

fn is_pinned(address: &str, pinned: &HashSet<Address>) -> bool {
    Address::from_str(address).map(|a| pinned.contains(&a)).unwrap_or(false)
}

Try / catch

match validate_deployment_manifest(&manifest) {
    Err(e) if e.to_string().contains("unpinned contract") => {
        eprintln!("Add the referenced contract to the manifest's contracts section: {e}");
    }
    Err(e) => return Err(e),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: Calling the deployment manifest validation with a pool whose address, token0, token1, factory, or quote_contract is not listed among the manifest's pinned contracts; this fires after the address parses as a valid Ethereum address, so it is purely a membership failure.

Common situations: Hand-editing a manifest and adding a pool without adding its token contracts to the contracts section; copying a pool from another chain/network manifest where the factory or quote contract has a different address; a contract being re-pinned to a new address so the pool's old reference no longer matches.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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