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
- Inspect the call trace to find which contract performed the forbidden operation and avoid that route/contract
- Regenerate the call manifest to explicitly cover the operations the route actually performs (if legitimate)
- Reject the swap quote and choose a different execution route
- 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
- Vet tokens and router routes for non-standard opcodes before listing
- Pre-validate simulated traces against the call manifest
- Reject routes that deploy or self-destruct anywhere in the call tree
- Keep call manifests in sync with the routes actually used
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Verified call trace child has an invalid caller context
- Verified call trace contains an unreviewed {call_type} edge
- Invalid venue {}, expected Blockchain DEX format
- `router_addresses` must contain at least one router address
- Quote spend limit for {token_in} -> {token_out} is denominat
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/6c709e077bd6e160.
Report an issue: GitHub.