Skip to main content

hopper_native/
cpi.rs

1//! Cross-program invocation via `sol_invoke_signed_c`.
2//!
3//! Provides both checked (borrow-validating) and unchecked invoke paths.
4
5use crate::account_view::AccountView;
6use crate::address::address_eq;
7use crate::error::ProgramError;
8use crate::instruction::{CpiAccount, InstructionView, Signer};
9use crate::ProgramResult;
10use core::mem::MaybeUninit;
11
12/// Maximum number of accounts in a static CPI call.
13pub const MAX_STATIC_CPI_ACCOUNTS: usize = 64;
14
15/// Maximum number of accounts in any CPI call.
16pub const MAX_CPI_ACCOUNTS: usize = 128;
17
18/// Maximum return data size (1 KiB).
19pub const MAX_RETURN_DATA: usize = 1024;
20
21// ── Unchecked invoke ─────────────────────────────────────────────────
22
23/// Invoke a CPI without borrow validation (lowest CU cost).
24///
25/// This is Tier C of the CPI surface. The checked variant
26/// ([`invoke`](crate::cpi::invoke)) enforces the full contract below
27/// before calling this function; prefer that unless you have measured
28/// a reason to bypass the validation pass.
29///
30/// # Safety
31///
32/// The caller must uphold every one of the following invariants. A
33/// violation of any of them is undefined behaviour, because the Solana
34/// runtime's `sol_invoke_signed_c` syscall assumes they already hold.
35///
36/// 1. **No aliasing borrows.** No `&` or `&mut` references into any
37///    account data region referenced by `accounts` may be live for
38///    the duration of the call. The CPI can (and will) mutate those
39///    regions via the callee, and Rust's aliasing rules do not permit
40///    the caller to hold outstanding references to memory that is
41///    about to change under it.
42/// 2. **Account list consistency.** Every `CpiAccount` in `accounts`
43///    must correspond to a real account previously passed to the
44///    program's entrypoint (same address, same `is_signer` /
45///    `is_writable` flags the runtime already knows about). The
46///    runtime will not re-derive account permissions; invalid flags
47///    propagate into the callee.
48/// 3. **Writability coverage.** Every account that the `instruction`
49///    marks writable must have `is_writable = true` in `accounts`,
50///    and every account the instruction marks as signer must have
51///    `is_signer = true`. Mismatches are rejected by the runtime but
52///    the rejection path is not cheap and the caller is expected to
53///    get this right.
54/// 4. **No shared mutable slices across CPIs.** If the same account
55///    appears more than once in `accounts` (duplicate accounts), the
56///    caller is responsible for ensuring that any subsequent borrow
57///    of that account's data respects the CPI's writes.
58/// 5. **Valid instruction encoding.** `instruction.program_id`,
59///    `instruction.accounts`, and `instruction.data` must all point
60///    to valid memory for the duration of the call. An
61///    `InstructionView` built from a local `InstructionAccount` slice
62///    is fine; one built from a dropped stack slot is not.
63///
64/// The runtime does not enforce any of these from the caller side —
65/// it assumes a well-formed CPI. That is the cost of the Tier C path.
66#[inline]
67pub unsafe fn invoke_unchecked(
68    instruction: &InstructionView,
69    accounts: &[CpiAccount],
70) -> ProgramResult {
71    #[cfg(target_os = "solana")]
72    {
73        // Build the C-ABI instruction struct on the stack.
74        // The Solana runtime expects:
75        //   struct { program_id: *const u8, accounts: *const SolAccountMeta, acct_len: u64, data: *const u8, data_len: u64 }
76        // But sol_invoke_signed_c takes the instruction as raw bytes.
77        let result = unsafe {
78            crate::syscalls::sol_invoke_signed_c(
79                instruction as *const _ as *const u8,
80                accounts.as_ptr() as *const u8,
81                accounts.len() as u64,
82                core::ptr::null(),
83                0,
84            )
85        };
86        if result == 0 {
87            Ok(())
88        } else {
89            Err(ProgramError::from(result))
90        }
91    }
92    #[cfg(not(target_os = "solana"))]
93    {
94        let _ = (instruction, accounts);
95        Ok(())
96    }
97}
98
99/// Invoke a signed CPI without borrow validation.
100///
101/// Same as [`invoke_unchecked`] but also passes PDA signer seeds so
102/// the callee can accept writes that would otherwise require a
103/// signature.
104///
105/// # Safety
106///
107/// All of [`invoke_unchecked`]'s invariants apply, plus two more for
108/// the signer-seeds path:
109///
110/// 6. **Signer seeds must derive the claimed PDA.** For every
111///    `Signer` in `signers_seeds`, the derived address
112///    (sha256 of `seeds || program_id || PDA_MARKER`) must equal an
113///    address in `accounts` that is marked as signer. A mismatch will
114///    cause the runtime to reject the CPI, but the caller is expected
115///    to have verified this before reaching the Tier C path.
116/// 7. **Seed lifetime.** `signers_seeds` (and every `&[u8]` it points
117///    at) must outlive the call. Temporary seed slices built inside a
118///    function frame are fine; seeds referencing dropped storage are
119///    not.
120///
121/// For the happy path the caller should hold a `CpiValidator` or
122/// equivalent proof-object constructed by the checked path and let
123/// that drive both this function's inputs and the aliasing discipline
124/// required above.
125#[inline]
126pub unsafe fn invoke_signed_unchecked(
127    instruction: &InstructionView,
128    accounts: &[CpiAccount],
129    signers_seeds: &[Signer],
130) -> ProgramResult {
131    #[cfg(target_os = "solana")]
132    {
133        let result = unsafe {
134            crate::syscalls::sol_invoke_signed_c(
135                instruction as *const _ as *const u8,
136                accounts.as_ptr() as *const u8,
137                accounts.len() as u64,
138                signers_seeds.as_ptr() as *const u8,
139                signers_seeds.len() as u64,
140            )
141        };
142        if result == 0 {
143            Ok(())
144        } else {
145            Err(ProgramError::from(result))
146        }
147    }
148    #[cfg(not(target_os = "solana"))]
149    {
150        let _ = (instruction, accounts, signers_seeds);
151        Ok(())
152    }
153}
154
155// ── CPI validation ───────────────────────────────────────────────────
156
157/// Validate that CPI account views match the instruction's expectations.
158///
159/// Checks:
160/// - Sufficient number of accounts.
161/// - Address identity (order-dependent matching).
162/// - Signer requirements.
163/// - Writable requirements.
164/// - Borrow compatibility (writable accounts must not be already borrowed,
165///   read-only accounts must not be exclusively borrowed).
166#[inline]
167fn validate_cpi_accounts(
168    instruction: &InstructionView,
169    account_views: &[&AccountView],
170) -> ProgramResult {
171    if account_views.len() < instruction.accounts.len() {
172        return Err(ProgramError::NotEnoughAccountKeys);
173    }
174
175    let mut i = 0;
176    while i < instruction.accounts.len() {
177        let expected = &instruction.accounts[i];
178        let actual = account_views[i];
179
180        if !address_eq(actual.address(), expected.address) {
181            return Err(ProgramError::InvalidAccountData);
182        }
183
184        if expected.is_signer && !actual.is_signer() {
185            return Err(ProgramError::MissingRequiredSignature);
186        }
187
188        if expected.is_writable && !actual.is_writable() {
189            return Err(ProgramError::Immutable);
190        }
191
192        // Borrow compatibility: writable needs exclusive access,
193        // read-only needs at least shared access.
194        if expected.is_writable {
195            actual.check_borrow_mut()?;
196        } else {
197            actual.check_borrow()?;
198        }
199
200        i += 1;
201    }
202
203    Ok(())
204}
205
206// ── Checked invoke ───────────────────────────────────────────────────
207
208/// Invoke a CPI with full validation.
209///
210/// Validates account count, address identity, signer/writable requirements,
211/// and borrow compatibility before calling the runtime.
212#[inline]
213pub fn invoke<const ACCOUNTS: usize>(
214    instruction: &InstructionView,
215    account_views: &[&AccountView; ACCOUNTS],
216) -> ProgramResult {
217    invoke_signed::<ACCOUNTS>(instruction, account_views, &[])
218}
219
220/// Invoke a signed CPI with full validation.
221///
222/// Validates account count, address identity, signer/writable requirements,
223/// and borrow compatibility before calling the runtime.
224#[inline]
225pub fn invoke_signed<const ACCOUNTS: usize>(
226    instruction: &InstructionView,
227    account_views: &[&AccountView; ACCOUNTS],
228    signers_seeds: &[Signer],
229) -> ProgramResult {
230    validate_cpi_accounts(instruction, &account_views[..])?;
231
232    // Build CpiAccount array on the stack.
233    let mut cpi_accounts: [MaybeUninit<CpiAccount>; ACCOUNTS] =
234        unsafe { MaybeUninit::uninit().assume_init() };
235
236    let mut i = 0;
237    while i < ACCOUNTS {
238        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
239        i += 1;
240    }
241
242    // SAFETY: All ACCOUNTS slots are now initialized.
243    let accounts: &[CpiAccount; ACCOUNTS] =
244        unsafe { &*(cpi_accounts.as_ptr() as *const [CpiAccount; ACCOUNTS]) };
245
246    unsafe {
247        if signers_seeds.is_empty() {
248            invoke_unchecked(instruction, accounts.as_slice())
249        } else {
250            invoke_signed_unchecked(instruction, accounts.as_slice(), signers_seeds)
251        }
252    }
253}
254
255/// Invoke with a dynamic number of accounts (bounded by const generic).
256#[inline]
257pub fn invoke_with_bounds<const MAX_ACCOUNTS: usize>(
258    instruction: &InstructionView,
259    account_views: &[&AccountView],
260) -> ProgramResult {
261    invoke_signed_with_bounds::<MAX_ACCOUNTS>(instruction, account_views, &[])
262}
263
264/// Signed invoke with a dynamic number of accounts (bounded by const generic).
265///
266/// Returns `Err(InvalidArgument)` if `account_views.len() > MAX_ACCOUNTS`.
267/// Validates accounts before invoking.
268#[inline]
269pub fn invoke_signed_with_bounds<const MAX_ACCOUNTS: usize>(
270    instruction: &InstructionView,
271    account_views: &[&AccountView],
272    signers_seeds: &[Signer],
273) -> ProgramResult {
274    if account_views.len() > MAX_ACCOUNTS {
275        return Err(ProgramError::InvalidArgument);
276    }
277
278    validate_cpi_accounts(instruction, account_views)?;
279
280    let mut cpi_accounts: [MaybeUninit<CpiAccount>; MAX_ACCOUNTS] =
281        unsafe { MaybeUninit::uninit().assume_init() };
282
283    let count = account_views.len();
284    let mut i = 0;
285    while i < count {
286        cpi_accounts[i] = MaybeUninit::new(CpiAccount::from(account_views[i]));
287        i += 1;
288    }
289
290    // SAFETY: first `count` slots are initialized.
291    let accounts =
292        unsafe { core::slice::from_raw_parts(cpi_accounts.as_ptr() as *const CpiAccount, count) };
293
294    unsafe {
295        if signers_seeds.is_empty() {
296            invoke_unchecked(instruction, accounts)
297        } else {
298            invoke_signed_unchecked(instruction, accounts, signers_seeds)
299        }
300    }
301}
302
303// ── Return data ──────────────────────────────────────────────────────
304
305/// Set return data for the current instruction.
306#[inline(always)]
307pub fn set_return_data(data: &[u8]) {
308    #[cfg(target_os = "solana")]
309    unsafe {
310        crate::syscalls::sol_set_return_data(data.as_ptr(), data.len() as u64);
311    }
312    #[cfg(not(target_os = "solana"))]
313    {
314        let _ = data;
315    }
316}