{"record":{"id":"402a4a0a4c773c48","repo":"FuelLabs/fuel-core","slug":"the-asset-has-multiple-change-policies","errorCode":null,"errorMessage":"The asset {} has multiple change policies","messagePattern":"The asset (.+?) has multiple change policies","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/fuel-core/src/schema/tx/assemble_tx.rs","lineNumber":311,"sourceCode":"                    amount: 0,\n                    change_policy: ChangePolicy::Change(recipient),\n                });\n            }\n        }\n\n        if base_asset_reserved.is_none() {\n            base_asset_reserved = Some(0);\n        }\n\n        for required_balance in &arguments.required_balances {\n            let asset_id = required_balance.asset_id;\n\n            let entry = change_output_policies.entry(asset_id);\n\n            match entry {\n                Entry::Occupied(old) => {\n                    if old.get() != &required_balance.change_policy {\n                        return Err(anyhow::anyhow!(\n                            \"The asset {} has multiple change policies\",\n                            asset_id\n                        ));\n                    }\n                }\n                Entry::Vacant(vacant) => {\n                    vacant.insert(required_balance.change_policy);\n                }\n            }\n        }\n\n        // Removed required balances with zero amount and if they are not used in inputs\n        let required_balances = core::mem::take(&mut arguments.required_balances);\n        arguments.required_balances = required_balances\n            .into_iter()\n            .filter_map(|r| {\n                if r.amount != 0 || unique_used_asset.contains(&r.asset_id) {\n                    Some(r)","sourceCodeStart":293,"sourceCodeEnd":329,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/fuel-core/src/schema/tx/assemble_tx.rs#L293-L329","documentation":"Each asset gets exactly one change policy: either the Change output the tx already carries for that asset, or the change_policy attached to a required balance. The assembler errors when a required balance's change policy conflicts with the policy already registered for the same asset (from a pre-existing Change output in your tx or from an earlier required balance).","triggerScenarios":"Submitting a tx that already contains Output::Change { asset_id: A, to: owner1 } together with a requiredBalances entry for asset A whose changeAccount.owner() differs; or mixing ChangePolicy::Change for asset A with ChangePolicy::Destroy for the same asset.","commonSituations":"Wallets that pre-attach change outputs and then also set changeAccount in requiredBalances; switching an asset to destroy-change while the template tx still emits a Change output; two required balances for the same asset (different accounts) with different change policies.","solutions":["Remove pre-existing Change outputs for assets you also declare in requiredBalances — the assembler creates change outputs itself","Align the changeAccount owner of the required balance with the recipient of the existing Change output for that asset","Pick one policy per asset: Change(to one owner) or Destroy, never both"],"exampleFix":"// before\nconst tx = new ScriptTransaction();\ntx.addOutput(new OutputChange(assetA, ownerOld));\nawait assemble(tx, { requiredBalances: [{ account: ownerNew, assetId: assetA, ... }] });\n\n// after\nconst tx = new ScriptTransaction(); // no manual Change output for assetA\nawait assemble(tx, { requiredBalances: [{ account: ownerNew, assetId: assetA, changeAccount: ownerNew, ... }] });","handlingStrategy":"validation","validationCode":"function assertNoPolicyConflict(tx: { outputs: Array<{ type: string; assetId?: string; to?: string }> }, balances: Array<{ assetId: string; changeAccount: string | null }>) {\n  const changeOwners = new Map(tx.outputs.filter(o => o.type === 'Change').map(o => [o.assetId!, o.to!]));\n  for (const b of balances) {\n    const existing = changeOwners.get(b.assetId);\n    const policyOwner = b.changeAccount ?? 'Destroy';\n    if (existing && existing !== policyOwner) {\n      throw new Error(`asset ${b.assetId} has conflicting change policies`);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"catch (e) { if (/has multiple change policies/.test(e.message)) { /* drop manual Change outputs for that asset or align changeAccount, then retry */ } else throw e; }","preventionTips":["Do not pre-attach Change outputs for assets declared in requiredBalances","Keep one change policy decision per asset in one place in your tx-building code","Beware: policy equality is by owner address — a different owner means a conflict"],"tags":["transaction","change-output","validation","graphql","assemble-tx"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}