nautechsystems/nautilus_trader · error

Verified call trace contains a forbidden state-changing oper

Error message

Verified call trace contains a forbidden state-changing operation

What it means

When validating a verified call trace against a call manifest, an operation in the trace has a call type of Create, Create2, or Selfdestruct — operations that mutate state and are never permitted. The client rejects the trace outright since only value-reading call types (call, callcode, delegatecall, staticcall) may appear.

Source

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

    caller_context: Address,
    purpose: &str,
    manifest: &BlockchainDeploymentManifest,
) -> anyhow::Result<()> {
    for call in calls {
        anyhow::ensure!(
            call.from == caller_context,
            "Verified call trace child has an invalid caller context"
        );
        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")

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the call trace to find which contract performed the forbidden operation and avoid that route/contract
  2. Regenerate the call manifest to explicitly cover the operations the route actually performs (if legitimate)
  3. Reject the swap quote and choose a different execution route
  4. Verify the target token/contract is a standard implementation without create/selfdestruct logic

Example fix

// before: accepting any quoted route
let plan = SwapPlan::from_quote(quote);
// after: filter routes with forbidden ops before validation
if quote.trace_ops.iter().any(|op| matches!(op, Op::Create | Op::Create2 | Op::Selfdestruct)) {
    quote.reject();
}
let plan = SwapPlan::from_quote(quote);
Defensive patterns

Strategy: validation

Validate before calling

fn trace_has_forbidden_ops(trace: &[CallFrame]) -> bool {
    trace.iter().any(|f| matches!(f.call_type, RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct))
}

Type guard

fn is_readonly_call(ct: &RpcCallType) -> bool {
    matches!(ct, RpcCallType::Call | RpcCallType::Callcode | RpcCallType::Delegatecall | RpcCallType::Staticcall)
}

Try / catch

match res {
    Err(e) if e.to_string().contains("forbidden state-changing") => {
        reject_quote_and_pick_alternate_route()?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Running an eth_call-style simulation whose resulting trace contains contract creation or selfdestruct frames while checking it against a manifest of permitted call edges — e.g. simulating a token swap where the token contract or a malicious/approved contract deploys or self-destructs mid-call.

Common situations: Interacting with an unusual or malicious token/contract that self-destructs during the call; a quoted swap route that routes through a deployer contract; manifest expectations built for pure read calls but the route performs state changes.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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