FuelLabs/fuel-core · error

fee index out of bounds

Error message

fee index out of bounds

What it means

A second, defensive lookup of the fee payer: when assembly begins it fetches required_balances[fee_index] to get the account that covers gas, erroring if the index is out of bounds. Through the GraphQL API this is normally unreachable because AssembleTx::new already validated fee_index < required_balances.len() at construction.

Source

Thrown at crates/fuel-core/src/schema/tx/assemble_tx.rs:268

            }
        }

        let mut change_output_policies = HashMap::<AssetId, ChangePolicy>::new();
        let mut set_change_outputs = HashSet::<AssetId>::new();

        for output in tx.outputs() {
            if let Output::Change { to, asset_id, .. } = output {
                change_output_policies.insert(*asset_id, ChangePolicy::Change(*to));
                set_change_outputs.insert(*asset_id);
            }
        }

        let mut base_asset_reserved: Option<u64> = None;

        let fee_payer_account = arguments
            .required_balances
            .get(arguments.fee_index as usize)
            .ok_or_else(|| anyhow::anyhow!("fee index out of bounds"))?
            .account
            .clone();

        let mut requested_asset = HashSet::new();
        for required_balance in &arguments.required_balances {
            let asset_id = required_balance.asset_id;
            requested_asset.insert(asset_id);

            if asset_id == base_asset_id
                && fee_payer_account.owner() == required_balance.account.owner()
            {
                base_asset_reserved = Some(required_balance.amount);
            }
        }

        for input_asset_id in &unique_used_asset {
            // If the user didn't request the asset, we add it to the required balances
            // with minimal amount `0` and `ChangePolicy::Change` policy.

View on GitHub (pinned to b9d4d170da)

Solutions

  1. Do not modify AssembleArguments.required_balances after AssembleTx::new — treat the arguments as consumed
  2. Rebuild the assembler with a fresh AssembleArguments whenever the balances list changes
  3. Keep the same fee_index < required_balances.len() invariant you already validated at construction

Example fix

// Rust embedder
// before
let mut assembler = AssembleTx::new(tx, args)?;
assembler.arguments_mut().required_balances.retain(|b| b.amount > 0); // may drop the fee payer
let tx = assembler.assemble().await?;

// after
let mut args = args;
args.required_balances.retain(|b| b.amount > 0);
args.fee_index = args.fee_index.min((args.required_balances.len() as u16).saturating_sub(1));
let tx = AssembleTx::new(tx, args)?.assemble().await?;
Defensive patterns

Strategy: validation

Validate before calling

// Rust embedder: validate immediately before assembly, not just at construction
fn args_valid(args: &AssembleArguments<'_>) -> bool {
    (args.fee_index as usize) < args.required_balances.len()
}

Try / catch

catch (e) { if (/fee index out of bounds/.test(e.message)) { /* rebuild AssembleArguments with a consistent fee_index */ } else throw e; }

Prevention

When it happens

Trigger: Practically limited to code embedding the assembler directly (AssembleTx::new + assemble) that mutates arguments.required_balances between construction and the assembly step, shrinking it below fee_index.

Common situations: Library embedders reusing an AssembleArguments struct after filtering its required_balances; test harnesses that drain the vec before calling assemble.

Related errors


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/d9c10344d0e2d3aa. Report an issue: GitHub.