Skip to main content

hopper_core/check/
guards.rs

1//! Security guard packs -- safe-by-default exploit prevention.
2//!
3//! Pre-built validation bundles for common exploit classes:
4//! - Account role mismatches (signer/writable/owner)
5//! - Post-mutation conservation (balance invariants)
6//! - Duplicate account detection
7//! - Instruction introspection guards (flash loan, re-entrancy)
8
9use hopper_runtime::error::ProgramError;
10use hopper_runtime::{AccountAudit, AccountView, Address, ProgramResult};
11
12// -- Account Role Guards ----------------------------------------------
13
14/// Validate a payer account: must be signer + writable.
15#[inline(always)]
16pub fn require_payer(account: &AccountView) -> ProgramResult {
17    if !account.is_signer() {
18        return Err(ProgramError::MissingRequiredSignature);
19    }
20    if !account.is_writable() {
21        return Err(ProgramError::InvalidAccountData);
22    }
23    Ok(())
24}
25
26/// Validate an authority account: must be signer, owned by expected program.
27#[inline(always)]
28pub fn require_authority(account: &AccountView, stored_authority: &[u8; 32]) -> ProgramResult {
29    if !account.is_signer() {
30        return Err(ProgramError::MissingRequiredSignature);
31    }
32    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
33    let addr: &[u8; 32] = unsafe {
34        // SAFETY: Address is [u8; 32].
35        &*(account.address() as *const Address as *const [u8; 32])
36    };
37    if !crate::check::keys_eq_fast(addr, stored_authority) {
38        return Err(ProgramError::InvalidAccountData);
39    }
40    Ok(())
41}
42
43/// Validate a writable program-owned account.
44#[inline(always)]
45pub fn require_owned_writable(account: &AccountView, program_id: &Address) -> ProgramResult {
46    if !account.owned_by(program_id) {
47        return Err(ProgramError::IncorrectProgramId);
48    }
49    if !account.is_writable() {
50        return Err(ProgramError::InvalidAccountData);
51    }
52    Ok(())
53}
54
55// -- Duplicate Account Detection --------------------------------------
56
57/// Verify that all accounts in a slice have unique addresses.
58///
59/// O(n²) but n is small (typically < 16). Prevents double-spend and
60/// confused-deputy attacks from duplicate account passing.
61#[inline]
62pub fn require_all_unique(accounts: &[AccountView]) -> ProgramResult {
63    AccountAudit::new(accounts).require_all_unique()
64}
65
66/// Verify that no duplicated account is writable.
67#[inline]
68pub fn require_unique_writable(accounts: &[AccountView]) -> ProgramResult {
69    AccountAudit::new(accounts).require_unique_writable()
70}
71
72/// Verify that no duplicated account is used as a signer.
73#[inline]
74pub fn require_unique_signers(accounts: &[AccountView]) -> ProgramResult {
75    AccountAudit::new(accounts).require_unique_signers()
76}
77
78// -- Post-Mutation Conservation ---------------------------------------
79
80/// Verify SOL conservation: total lamports before == total lamports after.
81///
82/// Call with pre-mutation snapshots of lamport values and the current
83/// account views. Detects lamport creation/destruction bugs.
84#[inline]
85pub fn check_lamport_conservation(accounts: &[AccountView], pre_lamports: &[u64]) -> ProgramResult {
86    if accounts.len() != pre_lamports.len() {
87        return Err(ProgramError::InvalidArgument);
88    }
89    let mut pre_total: u64 = 0;
90    let mut post_total: u64 = 0;
91    let mut i = 0;
92    while i < accounts.len() {
93        pre_total = pre_total
94            .checked_add(pre_lamports[i])
95            .ok_or(ProgramError::ArithmeticOverflow)?;
96        post_total = post_total
97            .checked_add(accounts[i].lamports())
98            .ok_or(ProgramError::ArithmeticOverflow)?;
99        i += 1;
100    }
101    if pre_total != post_total {
102        return Err(ProgramError::InvalidAccountData);
103    }
104    Ok(())
105}
106
107/// Snapshot lamport values for conservation checking.
108///
109/// Returns a stack-allocated array of lamport values.
110/// `N` must match the number of accounts tracked.
111#[inline]
112pub fn snapshot_lamports<const N: usize>(
113    accounts: &[AccountView],
114) -> Result<[u64; N], ProgramError> {
115    if accounts.len() < N {
116        return Err(ProgramError::NotEnoughAccountKeys);
117    }
118    let mut snapshot = [0u64; N];
119    let mut i = 0;
120    while i < N {
121        snapshot[i] = accounts[i].lamports();
122        i += 1;
123    }
124    Ok(snapshot)
125}
126
127// -- Signer-Writable Coherence ----------------------------------------
128
129/// Validate that every writable account in the slice is also a signer
130/// OR is owned by our program.
131///
132/// Prevents fee-drain attacks where an attacker passes a writable
133/// account they don't own, hoping the program modifies it.
134#[inline]
135pub fn check_writable_coherence(accounts: &[AccountView], program_id: &Address) -> ProgramResult {
136    let mut i = 0;
137    while i < accounts.len() {
138        if accounts[i].is_writable()
139            && !accounts[i].is_signer()
140            && !accounts[i].owned_by(program_id)
141        {
142            return Err(ProgramError::InvalidAccountData);
143        }
144        i += 1;
145    }
146    Ok(())
147}