---
title: Cross-Program Invocation
description: Invoke another Thru program from Rust with AccountManager::invoke
  and delegate authority with InvokeAuth.
source_url:
  html: https://thru.org/docs/sdks/rust-program-sdk/cross-program-invocation/
  md: https://thru.org/docs/sdks/rust-program-sdk/cross-program-invocation.md
---

# Cross-Program Invocation

Use this page when a Rust program has to call another program in the same transaction.

## Invoking a program

```rust
use thru_core::syscall::SyscallCode;
use thru_core::{program_utils, AccountManager};
use thru_sdk_macros::*;

#[entry(stack_size = 4096)]
fn main(instr_data: &[u8], mgr: AccountManager<2>) -> Result<u64, u64> {
    /* index of the callee program account in this transaction */
    const CALLEE_IDX: u16 = 2;

    let (invoke_result, callee_result) = mgr.invoke(CALLEE_IDX, instr_data, None);

    if invoke_result != SyscallCode::Success {
        program_utils::revert(invoke_result as i64 as u64);
    }

    match callee_result {
        SyscallCode::Success => Ok(0),
        _ => program_utils::revert(callee_result as i64 as u64),
    }
}
```

`invoke` takes the callee’s account index, the instruction data to pass through, and an optional [`InvokeAuth`](#delegating-authority). It returns two codes in this order: the syscall-level invoke result, then the callee’s exit code. Check both — a successful syscall can still return a callee revert.

Both codes are `SyscallCode`. Cast through `i64` before turning one into a revert code, so the negative VM value is preserved.

## Borrows must be released first

`invoke` panics if any account is still borrowed, because the callee may write to any writable account in the transaction. Drop every `AccountRef` before calling it:

```rust
{
    let account = mgr.get(2)?;
    let flag = account.data()[0];
    /* account is dropped here */
}

let (invoke_result, callee_result) = mgr.invoke(CALLEE_IDX, instr_data, None);
```

`mgr.has_active_borrows()` tells you whether a borrow is still live.

## Call depth

The VM caps invocation depth; exceeding it returns `SyscallCode::CallDepthTooDeep`. Read the current depth from the shadow stack, and use `program_utils::is_program_reentrant()` to detect that the current program already appears in a caller frame:

```rust
use thru_core::{get_shadow_stack, program_utils};

let depth = get_shadow_stack().call_depth();
let reentrant = program_utils::is_program_reentrant();
```

## Delegating authority

By default the callee treats as authorized only the fee payer, the programs in the invocation chain, and accounts a caller frame delegated to it. To delegate accounts your program owns, pass an `InvokeAuth`:

```rust
pub struct InvokeAuth {
    pub magic: u64,      /* must be INVOKE_AUTH_MAGIC */
    pub auth_cnt: u16,
    pub deauth_cnt: u16,
    /* auth_cnt + deauth_cnt u16 account indices follow in memory */
}
```

The authorized indices come first, then the de-authorized ones. `invoke` validates the structure before performing the syscall and reverts when:

- `magic` is not `INVOKE_AUTH_MAGIC` (revert code `0xBAD0A170`)
- an authorized account is not owned by the calling program (`0xBAD0A171`)
- an index is out of range (`0xBAD0A173`)

De-authorized indices suppress authority the callee would otherwise inherit, and are checked before authorizations when the callee evaluates `program_utils::is_account_authorized_by_idx`.

## Checking authorization in the callee

```rust
use thru_core::program_utils;

if !program_utils::is_account_authorized_by_idx(owner_idx) {
    return Err(1);
}
```

Use `is_account_authorized_by_pubkey(&pubkey)` when you have an address instead of an index, and `is_account_idx_owned_by_current_program(idx)` to check ownership before mutating.

## Related pages

- [Cross-program invocation guide](https://thru.org/docs/program-development/cross-program-invocation.md)
- [Accounts and Transaction Context](https://thru.org/docs/sdks/rust-program-sdk/accounts-and-transaction-context.md)
- [`invoke` syscall](https://thru.org/docs/spec/vm/syscalls/invoke.md)
