nautechsystems/nautilus_trader · critical · anyhow::Error

Verified call trace contains an unreviewed {call_type} edge

Error message

Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}

What it means

This error is thrown by `validate_internal_calls` when an internal call frame in the verified trace corresponds to a caller→target edge (for a given call type) that is not listed in the deployment manifest's reviewed `call_edges` for the execution `purpose`. The deployment manifest is an allowlist of contract-to-contract call edges that have been manually reviewed; any edge observed on chain that is not in the allowlist means the transaction performed an unreviewed interaction, which is rejected as a security control.

Source

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

        let target = call.to.ok_or_else(|| {
            anyhow::anyhow!("Verified call trace contains an operation without a target")
        })?;
        let call_type = match call.call_type {
            RpcCallType::Call => "call",
            RpcCallType::Callcode => "callcode",
            RpcCallType::Delegatecall => "delegatecall",
            RpcCallType::Staticcall => "staticcall",
            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
                anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
            }
        };
        let permitted = manifest.call_edges.iter().any(|edge| {
            edge.purpose == purpose
                && edge.call_type.eq_ignore_ascii_case(call_type)
                && Address::from_str(&edge.caller).ok() == Some(call.from)
                && Address::from_str(&edge.target).ok() == Some(target)
        });
        anyhow::ensure!(
            permitted,
            "Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}",
            call.from
        );
        let child_context = match call.call_type {
            RpcCallType::Call | RpcCallType::Staticcall => target,
            RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,
            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
                unreachable!("forbidden operations return before child traversal")
            }
        };
        validate_internal_calls(&call.calls, child_context, purpose, manifest)?;
    }
    Ok(())
}

async fn verify_wrap_balance_increase(
    executor: &TransactionExecutor,

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Review the reported `{call_type} edge {from} -> {target} for {purpose}` in the message; if the interaction is expected and safe, add a matching entry to `call_edges` in the deployment manifest (purpose, call_type, caller, target) and regenerate/redeploy the manifest.
  2. Verify the addresses in the manifest match the on-chain addresses exactly (checksum/lowercase is handled via `Address::from_str`, but wrong or stale addresses will never match); update stale entries after any contract upgrade or dependency swap.
  3. Confirm the `purpose` string passed to execution/verification exactly matches the purpose recorded on the manifest edges (case-sensitive).
  4. If the unexpected edge comes from a contract upgrade you did not intend, pin the expected bytecode/dependencies in the manifest verification and redeploy the correct contract version.
  5. Re-run the trace after updating the manifest; do not bypass or weaken the allowlist check.

Example fix

// before: manifest missing the newly introduced token edge
[[deployment.call_edges]]
purpose = "wrap"
call_type = "call"
caller = "0xrouter..."
target = "0xoldweth..."  # router now calls 0xnewweth...

// after: add the reviewed edge the trace observed
[[deployment.call_edges]]
purpose = "wrap"
call_type = "call"
caller = "0xrouter..."
target = "0xnewweth..."  # reviewed and approved
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check that every edge the trace will exercise is in the manifest allowlist.
fn edges_permitted(calls: &[VerifiedCallTrace], ctx: Address, purpose: &str, manifest: &BlockchainDeploymentManifest) -> bool {
    calls.iter().all(|c| {
        let Some(target) = c.to else { return false };
        let call_type = match c.call_type {
            RpcCallType::Call => "call",
            RpcCallType::Callcode => "callcode",
            RpcCallType::Delegatecall => "delegatecall",
            RpcCallType::Staticcall => "staticcall",
            _ => return false,
        };
        manifest.call_edges.iter().any(|e| {
            e.purpose == purpose
                && e.call_type.eq_ignore_ascii_case(call_type)
                && Address::from_str(&e.caller).ok() == Some(c.from)
                && Address::from_str(&e.target).ok() == Some(target)
        })
    })
}

Type guard

fn edge_is_reviewed(edge: &ManifestCallEdge, purpose: &str, from: Address, target: Address, call_type: &str) -> bool {
    edge.purpose == purpose
        && edge.call_type.eq_ignore_ascii_case(call_type)
        && Address::from_str(&edge.caller).ok() == Some(from)
        && Address::from_str(&edge.target).ok() == Some(target)
}

Try / catch

match result {
    Err(e) if e.to_string().starts_with("Verified call trace contains an unreviewed") => {
        // surface the edge for security review; add to call_edges only after human approval
        request_manifest_review(e.to_string());
        Err(e)
    }
    other => other,
}

Prevention

When it happens

Trigger: `anyhow::ensure!(permitted, ...)` at client.rs:4445 fails when no entry in `manifest.call_edges` matches all of: `edge.purpose == purpose`, `edge.call_type` (case-insensitive) equals the frame's call type (`call`/`callcode`/`delegatecall`/`staticcall`), `edge.caller` parses to the frame's `from` address, and `edge.target` parses to the frame's `to` address. Triggered whenever a traced internal call touches a contract pair not reviewed for that purpose.

Common situations: A deployed contract was upgraded or its dependencies changed so it now calls a new contract address not in the reviewed manifest; the manifest was generated before a dependency (e.g. a token or router) was swapped; running an execution with a `purpose` whose edges were never added to the manifest; a mistyped or checksummed-vs-lowercase address in the manifest preventing a match; the trace legitimately follows a deeper edge that reviewers forgot to approve.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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