Skip to main content

hopper_core/check/
mod.rs

1//! Multi-tier validation system.
2//!
3//! Hopper supports five validation levels:
4//!
5//! 1. **Account-local**: owner, signer, writable, size, discriminator, layout_id
6//! 2. **Cross-account**: `vault.mint == mint.address()`, authority matches
7//! 3. **State-transition**: status enum transitions, balance bounds
8//! 4. **CPI composition**: post-CPI invariants, no-CPI guards
9//! 5. **Post-mutation**: balance conservation, solvency invariants (via `PostMutationValidator`)
10//!
11//! Validation can be composed with named groups (`ValidationGroup`), instruction-specific
12//! rule packs (`TransitionRulePack`), and multi-group bundles (`ValidationBundle`).
13
14pub mod fast;
15#[cfg(feature = "graph")]
16pub mod graph;
17pub mod guards;
18pub mod modifier;
19pub mod trust;
20
21use hopper_runtime::{
22    address::address_eq, error::ProgramError, AccountView, Address, ProgramResult,
23};
24
25// --- Tier 1: Account-Local -------------------------------------------
26
27/// Check that an account is a signer.
28#[inline(always)]
29pub fn check_signer(account: &AccountView) -> ProgramResult {
30    if !account.is_signer() {
31        return Err(ProgramError::MissingRequiredSignature);
32    }
33    Ok(())
34}
35
36/// Check that an account is writable.
37#[inline(always)]
38pub fn check_writable(account: &AccountView) -> ProgramResult {
39    if !account.is_writable() {
40        return Err(ProgramError::InvalidAccountData);
41    }
42    Ok(())
43}
44
45/// Check that an account is owned by the expected program.
46#[inline(always)]
47pub fn check_owner(account: &AccountView, expected: &Address) -> ProgramResult {
48    if !account.owned_by(expected) {
49        return Err(ProgramError::IncorrectProgramId);
50    }
51    Ok(())
52}
53
54/// Check that an account is executable (a program).
55#[inline(always)]
56pub fn check_executable(account: &AccountView) -> ProgramResult {
57    if !account.executable() {
58        return Err(ProgramError::InvalidAccountData);
59    }
60    Ok(())
61}
62
63/// Check that an account is a specific program: its key matches
64/// `expected_program_id` and it is flagged executable.
65///
66/// Matches the Jiminy-style free-function surface the winning-
67/// architecture design calls for. Equivalent to constructing a
68/// `Program<'info, P>` wrapper without requiring a generic
69/// [`ProgramId`](hopper_runtime::ProgramId) impl - useful for
70/// ad-hoc program pinning where the program ID is only known at
71/// runtime (for instance, a caller-supplied cross-program id).
72#[inline(always)]
73pub fn check_program(account: &AccountView, expected_program_id: &Address) -> ProgramResult {
74    if !address_eq(account.address(), expected_program_id) {
75        return Err(ProgramError::IncorrectProgramId);
76    }
77    if !account.executable() {
78        return Err(ProgramError::InvalidAccountData);
79    }
80    Ok(())
81}
82
83/// Check minimum data size.
84#[inline(always)]
85pub fn check_size(data: &[u8], min_len: usize) -> ProgramResult {
86    if data.len() < min_len {
87        return Err(ProgramError::AccountDataTooSmall);
88    }
89    Ok(())
90}
91
92/// Check that the discriminator byte matches.
93#[inline(always)]
94pub fn check_discriminator(data: &[u8], expected: u8) -> ProgramResult {
95    if data.is_empty() || data[0] != expected {
96        return Err(ProgramError::InvalidAccountData);
97    }
98    Ok(())
99}
100
101/// Check uninitialized: account data is empty.
102#[inline(always)]
103pub fn check_uninitialized(account: &AccountView) -> ProgramResult {
104    if !account.is_data_empty() {
105        return Err(ProgramError::AccountAlreadyInitialized);
106    }
107    Ok(())
108}
109
110/// Check that an account has not been closed (no close sentinel).
111#[inline(always)]
112pub fn check_not_closed(data: &[u8]) -> ProgramResult {
113    if !data.is_empty() && data[0] == crate::account::CLOSE_SENTINEL {
114        return Err(ProgramError::InvalidAccountData);
115    }
116    Ok(())
117}
118
119/// Rent-exempt minimum lamports for a given data size.
120#[inline(always)]
121pub fn rent_exempt_min(data_len: usize) -> u64 {
122    ((128 + data_len) as u64) * 6960
123}
124
125/// Check that an account is rent exempt.
126#[inline(always)]
127pub fn check_rent_exempt(account: &AccountView) -> ProgramResult {
128    let lamports = account.lamports();
129    let data = account.try_borrow()?;
130    let min = rent_exempt_min(data.len());
131    if lamports < min {
132        return Err(ProgramError::InsufficientFunds);
133    }
134    Ok(())
135}
136
137/// Check that the account has at least `min` lamports.
138#[inline(always)]
139pub fn check_lamports_gte(account: &AccountView, min: u64) -> ProgramResult {
140    let lamports = account.lamports();
141    if lamports < min {
142        return Err(ProgramError::InsufficientFunds);
143    }
144    Ok(())
145}
146
147// --- Tier 2: Cross-Account ------------------------------------------
148
149/// Check that two account addresses are equal.
150#[inline(always)]
151pub fn check_keys_eq(a: &AccountView, b: &AccountView) -> ProgramResult {
152    if !address_eq(a.address(), b.address()) {
153        return Err(ProgramError::InvalidAccountData);
154    }
155    Ok(())
156}
157
158/// Fast 32-byte key equality check using 4x u64 comparisons.
159///
160/// Short-circuits on the first differing 8-byte chunk, saving cycles
161/// vs byte-by-byte comparison for addresses that differ early.
162/// hopper-native-inspired optimization.
163#[inline(always)]
164pub fn keys_eq_fast(a: &[u8; 32], b: &[u8; 32]) -> bool {
165    // SAFETY: [u8; 32] is always valid for read_unaligned as u64.
166    // We compare 4 x u64 chunks with short-circuit evaluation.
167    unsafe {
168        let a_ptr = a.as_ptr() as *const u64;
169        let b_ptr = b.as_ptr() as *const u64;
170        core::ptr::read_unaligned(a_ptr) == core::ptr::read_unaligned(b_ptr)
171            && core::ptr::read_unaligned(a_ptr.add(1)) == core::ptr::read_unaligned(b_ptr.add(1))
172            && core::ptr::read_unaligned(a_ptr.add(2)) == core::ptr::read_unaligned(b_ptr.add(2))
173            && core::ptr::read_unaligned(a_ptr.add(3)) == core::ptr::read_unaligned(b_ptr.add(3))
174    }
175}
176
177/// Check if a 32-byte address is all zeros (the default/system address).
178///
179/// Uses an OR-fold: OR all 4 u64 chunks together, then check if the result is zero.
180/// This avoids 32 individual byte comparisons.
181#[inline(always)]
182pub fn is_zero_address(addr: &[u8; 32]) -> bool {
183    // SAFETY: [u8; 32] is always valid for read_unaligned as u64.
184    unsafe {
185        let ptr = addr.as_ptr() as *const u64;
186        let combined = core::ptr::read_unaligned(ptr)
187            | core::ptr::read_unaligned(ptr.add(1))
188            | core::ptr::read_unaligned(ptr.add(2))
189            | core::ptr::read_unaligned(ptr.add(3));
190        combined == 0
191    }
192}
193
194/// Check `has_one`: a stored address in account data matches another account's address.
195///
196/// `stored` is the 32-byte address stored in account data at a given offset.
197/// This is the Anchor-style `has_one` equivalent.
198#[inline(always)]
199pub fn check_has_one(stored: &[u8; 32], account: &AccountView) -> ProgramResult {
200    // SAFETY: Address is [u8; 32]. Reinterpret as reference.
201    let addr: &[u8; 32] = unsafe { &*(account.address() as *const Address as *const [u8; 32]) };
202    if !keys_eq_fast(stored, addr) {
203        return Err(ProgramError::InvalidAccountData);
204    }
205    Ok(())
206}
207
208/// Check that two accounts are unique (different addresses).
209#[inline(always)]
210pub fn check_accounts_unique(a: &AccountView, b: &AccountView) -> ProgramResult {
211    if address_eq(a.address(), b.address()) {
212        return Err(ProgramError::InvalidArgument);
213    }
214    Ok(())
215}
216
217/// Check that three accounts are all unique.
218#[inline(always)]
219pub fn check_accounts_unique_3(a: &AccountView, b: &AccountView, c: &AccountView) -> ProgramResult {
220    if address_eq(a.address(), b.address())
221        || address_eq(a.address(), c.address())
222        || address_eq(b.address(), c.address())
223    {
224        return Err(ProgramError::InvalidArgument);
225    }
226    Ok(())
227}
228
229/// Check an account's address matches an expected value.
230#[inline(always)]
231pub fn check_address(account: &AccountView, expected: &Address) -> ProgramResult {
232    if !address_eq(account.address(), expected) {
233        return Err(ProgramError::InvalidAccountData);
234    }
235    Ok(())
236}
237
238/// Check instruction data meets minimum length.
239#[inline(always)]
240pub fn check_instruction_data_min(data: &[u8], min: usize) -> ProgramResult {
241    if data.len() < min {
242        return Err(ProgramError::InvalidInstructionData);
243    }
244    Ok(())
245}
246
247// --- Tier 3: Combined Checks ----------------------------------------
248
249/// Combined account check: owner + discriminator + minimum size.
250///
251/// This is the most common validation pattern. One function call instead of three.
252#[inline(always)]
253pub fn check_account(
254    account: &AccountView,
255    program_id: &Address,
256    disc: u8,
257    min_size: usize,
258) -> ProgramResult {
259    check_owner(account, program_id)?;
260    let data = account.try_borrow()?;
261    check_size(&data, min_size)?;
262    check_discriminator(&data, disc)?;
263    Ok(())
264}
265
266/// System program check.
267#[inline(always)]
268pub fn check_system_program(account: &AccountView) -> ProgramResult {
269    // System program ID: 11111111111111111111111111111111
270    const SYSTEM_PROGRAM: Address = Address::new_from_array([0; 32]);
271    if *account.address() != SYSTEM_PROGRAM {
272        return Err(ProgramError::IncorrectProgramId);
273    }
274    Ok(())
275}
276
277// --- PDA Helpers ----------------------------------------------------
278
279/// Verify a PDA with bump, using the cheap `create_program_address` path.
280///
281/// This costs ~200 CU vs ~1500 CU for `find_program_address`.
282/// Always use this when you have the bump stored.
283#[inline(always)]
284pub fn verify_pda(
285    account: &AccountView,
286    seeds: &[&[u8]],
287    bump: u8,
288    program_id: &Address,
289) -> ProgramResult {
290    hopper_runtime::pda::verify_pda_with_bump(account, seeds, bump, program_id)
291}
292
293/// Find a PDA and verify it matches the account, returning the bump.
294///
295/// On Hopper Native this uses the fast PDA path (`sol_sha256` +
296/// `sol_curve_validate_point`), which is roughly ~544 CU for a first-try bump.
297/// Prefer `verify_pda` when bump is stored.
298#[inline(always)]
299pub fn find_and_verify_pda(
300    account: &AccountView,
301    seeds: &[&[u8]],
302    program_id: &Address,
303) -> Result<u8, ProgramError> {
304    hopper_runtime::pda::find_and_verify_pda(account, seeds, program_id)
305}
306
307// --- BUMP_OFFSET PDA Optimization ----------------------------------
308//
309// When a layout stores its PDA
310// bump in a known field, we can read it directly from account data and
311// call `create_program_address` (~200 CU) instead of `find_program_address`
312// (~544 CU). Saves ~344 CU per PDA validation.
313
314/// Verify a PDA by reading the bump from account data at a known offset.
315///
316/// This is the **BUMP_OFFSET optimization**: when a layout stores its PDA bump
317/// byte at a compile-time-known offset, we read it directly from account data
318/// and use `create_program_address` (~200 CU) instead of `find_program_address`
319/// (~544 CU). Saves ~344 CU per PDA check.
320///
321/// # Arguments
322/// - `account` -- The account to verify
323/// - `seeds` -- PDA seeds (without bump)
324/// - `bump_offset` -- Byte offset of the bump field in account data
325/// - `program_id` -- The owning program
326#[inline(always)]
327pub fn verify_pda_cached(
328    account: &AccountView,
329    seeds: &[&[u8]],
330    bump_offset: usize,
331    program_id: &Address,
332) -> ProgramResult {
333    #[cfg(target_os = "solana")]
334    {
335        let data = account.try_borrow()?;
336        if bump_offset >= data.len() {
337            return Err(ProgramError::AccountDataTooSmall);
338        }
339        let bump = data[bump_offset];
340        let bump_seed = [bump];
341        let mut all_seeds: [&[u8]; 17] = [&[]; 17];
342        let seed_count = seeds.len();
343        if seed_count > 16 {
344            return Err(ProgramError::InvalidSeeds);
345        }
346        let mut i = 0;
347        while i < seed_count {
348            all_seeds[i] = seeds[i];
349            i += 1;
350        }
351        all_seeds[seed_count] = &bump_seed;
352
353        let derived = Address::create_program_address(&all_seeds[..seed_count + 1], program_id)?;
354
355        if !address_eq(account.address(), &derived) {
356            return Err(ProgramError::InvalidSeeds);
357        }
358        Ok(())
359    }
360    #[cfg(not(target_os = "solana"))]
361    {
362        let _ = (account, seeds, bump_offset, program_id);
363        Err(ProgramError::InvalidSeeds)
364    }
365}
366
367// --- Multi-Owner Foreign Load --------------------------------------
368//
369// Interface<T> + ProgramInterface pattern.
370// Allows loading foreign accounts that could be owned by any of several
371// programs (e.g., Token program OR Token-2022).
372
373/// Check that an account is owned by one of the given program IDs.
374///
375/// Used for multi-program interfaces (e.g., Token vs Token-2022).
376/// Returns the index of the matching owner, or error.
377#[inline]
378pub fn check_owner_multi(
379    account: &AccountView,
380    owners: &[&Address],
381) -> Result<usize, ProgramError> {
382    // SAFETY: Reading the owner field is safe in the context of
383    // account validation; no conflicting borrows exist yet.
384    let acct_owner = unsafe { account.owner() };
385    for (i, expected) in owners.iter().enumerate() {
386        if acct_owner == *expected {
387            return Ok(i);
388        }
389    }
390    Err(ProgramError::IncorrectProgramId)
391}
392
393// --- Instruction Introspection Guards ------------------------------
394//
395// Ported and improved from Jiminy's instruction sysvar analysis.
396// Detects CPI re-entrancy, flash loans, and sandwich attacks.
397
398/// Instructions sysvar address (Sysvar1nstructions1111111111111111111111111).
399#[allow(dead_code)]
400const INSTRUCTIONS_SYSVAR: Address = {
401    // Sysvar1nstructions1111111111111111111111111
402    // This is the base58-decoded address
403    let mut addr = [0u8; 32];
404    addr[0] = 0x06;
405    addr[1] = 0xa7;
406    addr[2] = 0xd5;
407    addr[3] = 0x17;
408    addr[4] = 0x18;
409    addr[5] = 0x7b;
410    addr[6] = 0xd1;
411    addr[7] = 0x66;
412    addr[8] = 0x35;
413    addr[9] = 0xda;
414    addr[10] = 0xd4;
415    addr[11] = 0x04;
416    addr[12] = 0x55;
417    addr[13] = 0xfb;
418    addr[14] = 0x04;
419    addr[15] = 0x6e;
420    addr[16] = 0x12;
421    addr[17] = 0x46;
422    addr[18] = 0x00;
423    addr[19] = 0x00;
424    addr[20] = 0x00;
425    addr[21] = 0x00;
426    addr[22] = 0x00;
427    addr[23] = 0x00;
428    addr[24] = 0x00;
429    addr[25] = 0x00;
430    addr[26] = 0x00;
431    addr[27] = 0x00;
432    addr[28] = 0x00;
433    addr[29] = 0x00;
434    addr[30] = 0x00;
435    addr[31] = 0x00;
436    Address::new_from_array(addr)
437};
438
439/// Read the number of instructions in the current transaction.
440///
441/// The Instructions sysvar stores `num_instructions` as the first u16 LE
442/// at offset 0 in the serialized data.
443#[inline(always)]
444pub fn instruction_count(sysvar_data: &[u8]) -> Result<u16, ProgramError> {
445    if sysvar_data.len() < 2 {
446        return Err(ProgramError::InvalidAccountData);
447    }
448    Ok(u16::from_le_bytes([sysvar_data[0], sysvar_data[1]]))
449}
450
451/// Read the current instruction index from the Instructions sysvar.
452///
453/// The Instructions sysvar stores the current instruction index as the
454/// last u16 LE at offset `data.len() - 2`.
455#[inline(always)]
456pub fn current_instruction_index(sysvar_data: &[u8]) -> Result<u16, ProgramError> {
457    let len = sysvar_data.len();
458    if len < 2 {
459        return Err(ProgramError::InvalidAccountData);
460    }
461    Ok(u16::from_le_bytes([
462        sysvar_data[len - 2],
463        sysvar_data[len - 1],
464    ]))
465}
466
467#[inline(always)]
468fn instruction_offset(sysvar_data: &[u8], index: u16) -> Result<usize, ProgramError> {
469    let num_ix = instruction_count(sysvar_data)?;
470    if index >= num_ix {
471        return Err(ProgramError::InvalidArgument);
472    }
473    let offset_entry = 2usize
474        .checked_add((index as usize).saturating_mul(2))
475        .ok_or(ProgramError::ArithmeticOverflow)?;
476    if offset_entry + 2 > sysvar_data.len() {
477        return Err(ProgramError::InvalidAccountData);
478    }
479    Ok(u16::from_le_bytes([sysvar_data[offset_entry], sysvar_data[offset_entry + 1]]) as usize)
480}
481
482#[inline]
483fn instruction_layout(
484    sysvar_data: &[u8],
485    index: u16,
486) -> Result<(usize, usize, usize), ProgramError> {
487    let ix_offset = instruction_offset(sysvar_data, index)?;
488    if ix_offset + 2 > sysvar_data.len() {
489        return Err(ProgramError::InvalidAccountData);
490    }
491    let account_count =
492        u16::from_le_bytes([sysvar_data[ix_offset], sysvar_data[ix_offset + 1]]) as usize;
493    let accounts_offset = ix_offset + 2;
494    let metas_len = account_count
495        .checked_mul(33)
496        .ok_or(ProgramError::ArithmeticOverflow)?;
497    let program_id_offset = accounts_offset
498        .checked_add(metas_len)
499        .ok_or(ProgramError::ArithmeticOverflow)?;
500    if program_id_offset + 34 > sysvar_data.len() {
501        return Err(ProgramError::InvalidAccountData);
502    }
503    let data_len_offset = program_id_offset + 32;
504    let data_len = u16::from_le_bytes([
505        sysvar_data[data_len_offset],
506        sysvar_data[data_len_offset + 1],
507    ]) as usize;
508    let data_offset = data_len_offset + 2;
509    if data_offset
510        .checked_add(data_len)
511        .ok_or(ProgramError::ArithmeticOverflow)?
512        > sysvar_data.len()
513    {
514        return Err(ProgramError::InvalidAccountData);
515    }
516    Ok((account_count, program_id_offset, data_offset))
517}
518
519/// Typed borrowed view over the Instructions sysvar account data.
520pub struct InstructionsSysvar<'a> {
521    data: &'a [u8],
522}
523
524impl<'a> InstructionsSysvar<'a> {
525    /// Borrow serialized Instructions sysvar data.
526    #[inline(always)]
527    pub const fn new(data: &'a [u8]) -> Self {
528        Self { data }
529    }
530
531    /// Raw serialized sysvar bytes.
532    #[inline(always)]
533    pub const fn as_bytes(&self) -> &'a [u8] {
534        self.data
535    }
536
537    /// Number of instructions in the transaction.
538    #[inline(always)]
539    pub fn len(&self) -> Result<u16, ProgramError> {
540        instruction_count(self.data)
541    }
542
543    /// True when the sysvar reports zero instructions.
544    #[inline(always)]
545    pub fn is_empty(&self) -> Result<bool, ProgramError> {
546        Ok(self.len()? == 0)
547    }
548
549    /// Current instruction index.
550    #[inline(always)]
551    pub fn current_index(&self) -> Result<u16, ProgramError> {
552        current_instruction_index(self.data)
553    }
554
555    /// Borrow instruction `index` as a typed view.
556    #[inline(always)]
557    pub fn instruction(&self, index: u16) -> Result<IntrospectedInstruction<'a>, ProgramError> {
558        IntrospectedInstruction::new(self.data, index)
559    }
560
561    /// Borrow the current instruction as a typed view.
562    #[inline(always)]
563    pub fn current_instruction(&self) -> Result<IntrospectedInstruction<'a>, ProgramError> {
564        self.instruction(self.current_index()?)
565    }
566
567    /// Read the program id at instruction `index`.
568    #[inline(always)]
569    pub fn program_id_at(&self, index: u16) -> Result<Address, ProgramError> {
570        let bytes = read_program_id_at(self.data, index)?;
571        Ok(Address::new_from_array(bytes))
572    }
573}
574
575/// Typed borrowed view over one instruction inside the Instructions sysvar.
576pub struct IntrospectedInstruction<'a> {
577    data: &'a [u8],
578    account_count: usize,
579    accounts_offset: usize,
580    program_id_offset: usize,
581    data_offset: usize,
582    data_len: usize,
583}
584
585impl<'a> IntrospectedInstruction<'a> {
586    #[inline]
587    fn new(sysvar_data: &'a [u8], index: u16) -> Result<Self, ProgramError> {
588        let ix_offset = instruction_offset(sysvar_data, index)?;
589        let (account_count, program_id_offset, data_offset) =
590            instruction_layout(sysvar_data, index)?;
591        let data_len_offset = program_id_offset + 32;
592        let data_len = u16::from_le_bytes([
593            sysvar_data[data_len_offset],
594            sysvar_data[data_len_offset + 1],
595        ]) as usize;
596        Ok(Self {
597            data: sysvar_data,
598            account_count,
599            accounts_offset: ix_offset + 2,
600            program_id_offset,
601            data_offset,
602            data_len,
603        })
604    }
605
606    /// Number of account metas in this instruction.
607    #[inline(always)]
608    pub const fn account_count(&self) -> usize {
609        self.account_count
610    }
611
612    /// Instruction program id.
613    #[inline(always)]
614    pub fn program_id(&self) -> Address {
615        let mut bytes = [0u8; 32];
616        bytes.copy_from_slice(&self.data[self.program_id_offset..self.program_id_offset + 32]);
617        Address::new_from_array(bytes)
618    }
619
620    /// Instruction data bytes.
621    #[inline(always)]
622    pub fn instruction_data(&self) -> &'a [u8] {
623        &self.data[self.data_offset..self.data_offset + self.data_len]
624    }
625
626    /// Read account meta `index`.
627    #[inline]
628    pub fn account(&self, index: usize) -> Result<Option<InstructionAccountMeta>, ProgramError> {
629        if index >= self.account_count {
630            return Ok(None);
631        }
632        let meta_offset = index
633            .checked_mul(33)
634            .ok_or(ProgramError::ArithmeticOverflow)?;
635        let offset = self
636            .accounts_offset
637            .checked_add(meta_offset)
638            .ok_or(ProgramError::ArithmeticOverflow)?;
639        if offset + 33 > self.data.len() {
640            return Err(ProgramError::InvalidAccountData);
641        }
642        let mut pubkey = [0u8; 32];
643        pubkey.copy_from_slice(&self.data[offset + 1..offset + 33]);
644        Ok(Some(InstructionAccountMeta {
645            flags: self.data[offset],
646            pubkey,
647        }))
648    }
649}
650
651/// Account meta decoded from an Instructions sysvar entry.
652#[derive(Clone, Copy, Debug, PartialEq, Eq)]
653pub struct InstructionAccountMeta {
654    flags: u8,
655    pubkey: [u8; 32],
656}
657
658impl InstructionAccountMeta {
659    /// Raw meta flags.
660    #[inline(always)]
661    pub const fn flags(&self) -> u8 {
662        self.flags
663    }
664
665    /// The account public key bytes.
666    #[inline(always)]
667    pub const fn pubkey_bytes(&self) -> &[u8; 32] {
668        &self.pubkey
669    }
670
671    /// The account public key as a Hopper address.
672    #[inline(always)]
673    pub const fn address(&self) -> Address {
674        Address::new_from_array(self.pubkey)
675    }
676
677    /// True when the meta is marked signer.
678    #[inline(always)]
679    pub const fn is_signer(&self) -> bool {
680        self.flags & 0x01 != 0
681    }
682
683    /// True when the meta is marked writable.
684    #[inline(always)]
685    pub const fn is_writable(&self) -> bool {
686        self.flags & 0x02 != 0
687    }
688}
689
690/// Read the program_id of instruction at the given index.
691///
692/// Instructions sysvar layout:
693/// ```text
694/// [u16 num_instructions]
695/// [u16 offset_0] [u16 offset_1] ... [u16 offset_{n-1}]  <-- offset table
696/// [serialized instruction 0]
697/// [serialized instruction 1]
698/// ...
699/// [u16 current_instruction_index]  <-- last 2 bytes
700/// ```
701///
702/// Per-instruction layout at `sysvar_data[offset]`:
703/// ```text
704/// [u16 num_accounts]
705/// [u8 flags + [u8; 32] pubkey] * num_accounts  (33 bytes each)
706/// [u8; 32] program_id
707/// [u16 data_len]
708/// [u8; data_len] data
709/// ```
710#[inline]
711pub fn read_program_id_at(sysvar_data: &[u8], index: u16) -> Result<[u8; 32], ProgramError> {
712    let (_, program_id_offset, _) = instruction_layout(sysvar_data, index)?;
713    if program_id_offset + 32 > sysvar_data.len() {
714        return Err(ProgramError::InvalidAccountData);
715    }
716    let mut pid = [0u8; 32];
717    pid.copy_from_slice(&sysvar_data[program_id_offset..program_id_offset + 32]);
718    Ok(pid)
719}
720
721/// Require that the current instruction is top-level (not a CPI).
722///
723/// Checks: current instruction's program_id matches `our_program`.
724/// If called via CPI, the current instruction would have a different
725/// program_id, so this fails.
726#[inline]
727pub fn require_top_level(sysvar_data: &[u8], our_program: &Address) -> ProgramResult {
728    let current_idx = current_instruction_index(sysvar_data)?;
729    let pid = read_program_id_at(sysvar_data, current_idx)?;
730    if pid != *our_program.as_array() {
731        return Err(ProgramError::InvalidAccountData);
732    }
733    Ok(())
734}
735
736/// Detect flash-loan bracket: same program called before AND after current.
737///
738/// Returns `Err` if the pattern is detected (the program appears both
739/// before and after the current instruction index).
740#[inline]
741pub fn detect_flash_loan_bracket(sysvar_data: &[u8], our_program: &Address) -> ProgramResult {
742    let current_idx = current_instruction_index(sysvar_data)?;
743    let num_ix = instruction_count(sysvar_data)?;
744
745    let mut before = false;
746    let mut after = false;
747
748    let mut i: u16 = 0;
749    while i < num_ix {
750        if i == current_idx {
751            i += 1;
752            continue;
753        }
754        if let Ok(pid) = read_program_id_at(sysvar_data, i) {
755            if pid == *our_program.as_array() {
756                if i < current_idx {
757                    before = true;
758                } else {
759                    after = true;
760                }
761            }
762        }
763        i += 1;
764    }
765
766    if before && after {
767        return Err(ProgramError::InvalidAccountData);
768    }
769    Ok(())
770}
771
772/// Ensure our program is not invoked after the current instruction.
773///
774/// Prevents post-execution re-entrancy patterns.
775#[inline]
776pub fn check_no_subsequent_invocation(sysvar_data: &[u8], our_program: &Address) -> ProgramResult {
777    let current_idx = current_instruction_index(sysvar_data)?;
778    let num_ix = instruction_count(sysvar_data)?;
779
780    let mut i = current_idx + 1;
781    while i < num_ix {
782        if let Ok(pid) = read_program_id_at(sysvar_data, i) {
783            if pid == *our_program.as_array() {
784                return Err(ProgramError::InvalidAccountData);
785            }
786        }
787        i += 1;
788    }
789    Ok(())
790}