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
- 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.
- 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.
- Confirm the `purpose` string passed to execution/verification exactly matches the purpose recorded on the manifest edges (case-sensitive).
- 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.
- 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
- Regenerate and review the deployment manifest's call_edges after every contract upgrade or dependency change.
- Keep address entries canonical (checksummed or lowercase consistently) in the manifest to avoid parse mismatches.
- Use exact, stable `purpose` strings shared between execution, manifest, and verification.
- Dry-run/simulate executions against a fork first and diff observed edges against the allowlist before live submission.
- Never bypass or auto-extend the allowlist in response to this error; route new edges through manual review.
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
- Router {router} is not in the configured `router_addresses`
- Verified call trace contains a forbidden state-changing oper
- Verified call trace child has an invalid caller context
- Token {token} is not an input token in the configured `allow
- Token pair {token_in} -> {token_out} is not in the `allowed_
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/80b3b2ade21da812.
Report an issue: GitHub.