Skip to content

Cross-Program Invocation

View as Markdown

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

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. 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.

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:

{
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.

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:

use thru_core::{get_shadow_stack, program_utils};
let depth = get_shadow_stack().call_depth();
let reentrant = program_utils::is_program_reentrant();

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:

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.

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.