{"record":{"id":"6c709e077bd6e160","repo":"nautechsystems/nautilus_trader","slug":"verified-call-trace-contains-a-forbidden-state-cha","errorCode":null,"errorMessage":"Verified call trace contains a forbidden state-changing operation","messagePattern":"Verified call trace contains a forbidden state-changing operation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/execution/client.rs","lineNumber":4436,"sourceCode":"    caller_context: Address,\n    purpose: &str,\n    manifest: &BlockchainDeploymentManifest,\n) -> anyhow::Result<()> {\n    for call in calls {\n        anyhow::ensure!(\n            call.from == caller_context,\n            \"Verified call trace child has an invalid caller context\"\n        );\n        let target = call.to.ok_or_else(|| {\n            anyhow::anyhow!(\"Verified call trace contains an operation without a target\")\n        })?;\n        let call_type = match call.call_type {\n            RpcCallType::Call => \"call\",\n            RpcCallType::Callcode => \"callcode\",\n            RpcCallType::Delegatecall => \"delegatecall\",\n            RpcCallType::Staticcall => \"staticcall\",\n            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {\n                anyhow::bail!(\"Verified call trace contains a forbidden state-changing operation\")\n            }\n        };\n        let permitted = manifest.call_edges.iter().any(|edge| {\n            edge.purpose == purpose\n                && edge.call_type.eq_ignore_ascii_case(call_type)\n                && Address::from_str(&edge.caller).ok() == Some(call.from)\n                && Address::from_str(&edge.target).ok() == Some(target)\n        });\n        anyhow::ensure!(\n            permitted,\n            \"Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}\",\n            call.from\n        );\n        let child_context = match call.call_type {\n            RpcCallType::Call | RpcCallType::Staticcall => target,\n            RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,\n            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {\n                unreachable!(\"forbidden operations return before child traversal\")","sourceCodeStart":4418,"sourceCodeEnd":4454,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/blockchain/src/execution/client.rs#L4418-L4454","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: accepting any quoted route\nlet plan = SwapPlan::from_quote(quote);\n// after: filter routes with forbidden ops before validation\nif quote.trace_ops.iter().any(|op| matches!(op, Op::Create | Op::Create2 | Op::Selfdestruct)) {\n    quote.reject();\n}\nlet plan = SwapPlan::from_quote(quote);","handlingStrategy":"validation","validationCode":"fn trace_has_forbidden_ops(trace: &[CallFrame]) -> bool {\n    trace.iter().any(|f| matches!(f.call_type, RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct))\n}","typeGuard":"fn is_readonly_call(ct: &RpcCallType) -> bool {\n    matches!(ct, RpcCallType::Call | RpcCallType::Callcode | RpcCallType::Delegatecall | RpcCallType::Staticcall)\n}","tryCatchPattern":"match res {\n    Err(e) if e.to_string().contains(\"forbidden state-changing\") => {\n        reject_quote_and_pick_alternate_route()?;\n    }\n    other => other?,\n}","preventionTips":["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"],"tags":["blockchain","security","call-trace","validation"],"backgroundTag":"unsupported-operation","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}