Skip to main content

jiminy_core/check/
mod.rs

1//! Account and instruction validation checks.
2//!
3//! Every function returns `ProgramResult`: `Ok(())` on pass,
4//! an appropriate `ProgramError` variant on failure.
5
6pub mod pda;
7
8use pinocchio::{error::ProgramError, AccountView, Address, ProgramResult};
9
10#[cfg(feature = "programs")]
11use crate::programs;
12
13// ── Identity & permissions ───────────────────────────────────────────────────
14
15/// The canonical system program address (all-zero pubkey).
16const SYSTEM_PROGRAM_ID: Address = Address::new_from_array([0u8; 32]);
17
18/// Verify the account signed the transaction.
19#[inline(always)]
20pub fn check_signer(account: &AccountView) -> ProgramResult {
21    if !account.is_signer() {
22        return Err(ProgramError::MissingRequiredSignature);
23    }
24    Ok(())
25}
26
27/// Verify the account is marked writable in the transaction.
28#[inline(always)]
29pub fn check_writable(account: &AccountView) -> ProgramResult {
30    if !account.is_writable() {
31        return Err(ProgramError::InvalidArgument);
32    }
33    Ok(())
34}
35
36/// Verify the account is owned by `program_id`.
37#[inline(always)]
38pub fn check_owner(account: &AccountView, program_id: &Address) -> ProgramResult {
39    if !account.owned_by(program_id) {
40        return Err(ProgramError::IncorrectProgramId);
41    }
42    Ok(())
43}
44
45/// Verify the account's address equals the expected PDA.
46#[inline(always)]
47pub fn check_pda(account: &AccountView, expected: &Address) -> ProgramResult {
48    if *account.address() != *expected {
49        return Err(ProgramError::InvalidSeeds);
50    }
51    Ok(())
52}
53
54/// Verify the account is the canonical system program.
55#[inline(always)]
56pub fn check_system_program(account: &AccountView) -> ProgramResult {
57    if *account.address() != SYSTEM_PROGRAM_ID {
58        return Err(ProgramError::IncorrectProgramId);
59    }
60    Ok(())
61}
62
63/// Verify the account has no data (uninitialized). Prevents reinitialization attacks.
64#[inline(always)]
65pub fn check_uninitialized(account: &AccountView) -> ProgramResult {
66    if !account.is_data_empty() {
67        return Err(ProgramError::AccountAlreadyInitialized);
68    }
69    Ok(())
70}
71
72/// Verify the account is an executable program.
73#[inline(always)]
74pub fn check_executable(account: &AccountView) -> ProgramResult {
75    if !account.executable() {
76        return Err(ProgramError::IncorrectProgramId);
77    }
78    Ok(())
79}
80
81// ── Data shape ───────────────────────────────────────────────────────────────
82
83/// Verify account data is at least `min_len` bytes.
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/// Verify the first byte of account data matches the expected discriminator.
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/// Combined check: ownership + minimum size + discriminator.
102#[inline(always)]
103pub fn check_account(
104    account: &AccountView,
105    program_id: &Address,
106    discriminator: u8,
107    min_len: usize,
108) -> ProgramResult {
109    check_owner(account, program_id)?;
110    let data = account.try_borrow()?;
111    check_size(&data, min_len)?;
112    check_discriminator(&data, discriminator)?;
113    Ok(())
114}
115
116/// Verify the header version byte (`data[1]`) meets a minimum version.
117#[inline(always)]
118pub fn check_version(data: &[u8], min_version: u8) -> ProgramResult {
119    if data.len() < 2 {
120        return Err(ProgramError::AccountDataTooSmall);
121    }
122    if data[1] < min_version {
123        return Err(ProgramError::InvalidAccountData);
124    }
125    Ok(())
126}
127
128// ── Keys & addresses ─────────────────────────────────────────────────────────
129
130/// Verify two addresses are equal.
131#[inline(always)]
132pub fn check_keys_eq(a: &Address, b: &Address) -> ProgramResult {
133    if *a != *b {
134        return Err(ProgramError::InvalidArgument);
135    }
136    Ok(())
137}
138
139/// Verify that a stored address field matches an account's actual address.
140///
141/// Runtime equivalent of Anchor's `has_one` constraint.
142#[inline(always)]
143pub fn check_has_one(stored: &Address, account: &AccountView) -> ProgramResult {
144    if stored != account.address() {
145        return Err(ProgramError::InvalidArgument);
146    }
147    Ok(())
148}
149
150// ── Rent & lamports ──────────────────────────────────────────────────────────
151
152/// Approximate minimum lamports for rent exemption at the current mainnet rate.
153///
154/// Formula: `(128 + data_len) * 6960`
155#[inline(always)]
156pub fn rent_exempt_min(data_len: usize) -> u64 {
157    (128u64 + data_len as u64).saturating_mul(6960)
158}
159
160/// Verify an account holds enough lamports to be rent-exempt for its data size.
161#[inline(always)]
162pub fn check_rent_exempt(account: &AccountView) -> ProgramResult {
163    let data = account.try_borrow()?;
164    let min = rent_exempt_min(data.len());
165    drop(data);
166    if account.lamports() < min {
167        return Err(ProgramError::InsufficientFunds);
168    }
169    Ok(())
170}
171
172/// Verify `account` holds at least `min_lamports`.
173#[inline(always)]
174pub fn check_lamports_gte(account: &AccountView, min_lamports: u64) -> ProgramResult {
175    if account.lamports() < min_lamports {
176        return Err(ProgramError::InsufficientFunds);
177    }
178    Ok(())
179}
180
181/// Verify an account is fully closed: zero lamports and empty data.
182#[inline(always)]
183pub fn check_closed(account: &AccountView) -> ProgramResult {
184    if account.lamports() != 0 || !account.is_data_empty() {
185        return Err(ProgramError::InvalidAccountData);
186    }
187    Ok(())
188}
189
190// ── Instruction data ─────────────────────────────────────────────────────────
191
192/// Verify instruction data is exactly the expected length.
193#[inline(always)]
194pub fn check_instruction_data_len(data: &[u8], expected_len: usize) -> ProgramResult {
195    if data.len() != expected_len {
196        return Err(ProgramError::InvalidInstructionData);
197    }
198    Ok(())
199}
200
201/// Verify instruction data has at least N bytes.
202#[inline(always)]
203pub fn check_instruction_data_min(data: &[u8], min_len: usize) -> ProgramResult {
204    if data.len() < min_len {
205        return Err(ProgramError::InvalidInstructionData);
206    }
207    Ok(())
208}
209
210// ── Uniqueness ───────────────────────────────────────────────────────────────
211
212/// Verify two accounts have different addresses.
213#[inline(always)]
214pub fn check_accounts_unique_2(a: &AccountView, b: &AccountView) -> ProgramResult {
215    if a.address() == b.address() {
216        return Err(ProgramError::InvalidArgument);
217    }
218    Ok(())
219}
220
221/// Verify three accounts all have different addresses.
222#[inline(always)]
223pub fn check_accounts_unique_3(
224    a: &AccountView,
225    b: &AccountView,
226    c: &AccountView,
227) -> ProgramResult {
228    if a.address() == b.address()
229        || a.address() == c.address()
230        || b.address() == c.address()
231    {
232        return Err(ProgramError::InvalidArgument);
233    }
234    Ok(())
235}
236
237/// Verify four accounts all have different addresses.
238#[inline(always)]
239pub fn check_accounts_unique_4(
240    a: &AccountView,
241    b: &AccountView,
242    c: &AccountView,
243    d: &AccountView,
244) -> ProgramResult {
245    if a.address() == b.address()
246        || a.address() == c.address()
247        || a.address() == d.address()
248        || b.address() == c.address()
249        || b.address() == d.address()
250        || c.address() == d.address()
251    {
252        return Err(ProgramError::InvalidArgument);
253    }
254    Ok(())
255}
256
257// ── Assert helpers (folded from asserts.rs) ──────────────────────────────────
258
259/// Derive a PDA from seeds, verify it matches the account, return the bump.
260///
261/// Calls `find_program_address` (syscall on-chain).
262#[inline(always)]
263pub fn assert_pda(
264    account: &AccountView,
265    seeds: &[&[u8]],
266    program_id: &Address,
267) -> Result<u8, ProgramError> {
268    #[cfg(target_os = "solana")]
269    {
270        let (derived, bump) = Address::find_program_address(seeds, program_id);
271        if derived != *account.address() {
272            return Err(ProgramError::InvalidSeeds);
273        }
274        Ok(bump)
275    }
276    #[cfg(not(target_os = "solana"))]
277    {
278        let _ = (account, seeds, program_id);
279        Err(ProgramError::InvalidSeeds)
280    }
281}
282
283/// Verify a PDA matches when the bump is already known. Cheaper, single derivation.
284#[inline(always)]
285pub fn assert_pda_with_bump(
286    account: &AccountView,
287    seeds: &[&[u8]],
288    bump: u8,
289    program_id: &Address,
290) -> ProgramResult {
291    #[cfg(target_os = "solana")]
292    {
293        let bump_bytes = [bump];
294        let n = seeds.len();
295        let mut all_seeds: [&[u8]; 17] = [&[]; 17];
296        let mut i = 0;
297        while i < n {
298            all_seeds[i] = seeds[i];
299            i += 1;
300        }
301        all_seeds[n] = &bump_bytes;
302
303        let derived = Address::create_program_address(&all_seeds[..n + 1], program_id)
304            .map_err(|_| ProgramError::InvalidSeeds)?;
305        if derived != *account.address() {
306            return Err(ProgramError::InvalidSeeds);
307        }
308        Ok(())
309    }
310    #[cfg(not(target_os = "solana"))]
311    {
312        let _ = (account, seeds, bump, program_id);
313        Err(ProgramError::InvalidSeeds)
314    }
315}
316
317/// Verify a PDA derived from an external program's seeds. Returns the bump.
318#[inline(always)]
319pub fn assert_pda_external(
320    account: &AccountView,
321    seeds: &[&[u8]],
322    program_id: &Address,
323) -> Result<u8, ProgramError> {
324    assert_pda(account, seeds, program_id)
325}
326
327/// Verify the account is the SPL Token program (Token or Token-2022).
328#[cfg(feature = "programs")]
329#[inline(always)]
330pub fn assert_token_program(account: &AccountView) -> ProgramResult {
331    if *account.address() != programs::TOKEN && *account.address() != programs::TOKEN_2022 {
332        return Err(ProgramError::IncorrectProgramId);
333    }
334    Ok(())
335}
336
337/// Verify an account's address matches an expected address exactly.
338#[inline(always)]
339pub fn assert_address(account: &AccountView, expected: &Address) -> ProgramResult {
340    if *account.address() != *expected {
341        return Err(ProgramError::InvalidArgument);
342    }
343    Ok(())
344}
345
346/// Verify an account's address matches a known program id + is executable.
347#[inline(always)]
348pub fn assert_program(account: &AccountView, expected_program: &Address) -> ProgramResult {
349    if *account.address() != *expected_program {
350        return Err(ProgramError::IncorrectProgramId);
351    }
352    if !account.executable() {
353        return Err(ProgramError::IncorrectProgramId);
354    }
355    Ok(())
356}
357
358/// Verify an account has never been initialized (lamports == 0).
359#[inline(always)]
360pub fn assert_not_initialized(account: &AccountView) -> ProgramResult {
361    if account.lamports() != 0 {
362        return Err(ProgramError::AccountAlreadyInitialized);
363    }
364    Ok(())
365}
366
367// ── Program allowlist ────────────────────────────────────────────────────────
368
369/// Verify the account is owned by one of the programs in `allowed`.
370///
371/// Use this when accepting accounts from a known set of trusted programs.
372/// Rejects anything not in the list.
373///
374/// ```rust,ignore
375/// const ALLOWED: &[Address] = &[PROGRAM_A, PROGRAM_B];
376/// check_program_allowed(account, ALLOWED)?;
377/// ```
378#[inline(always)]
379pub fn check_program_allowed(
380    account: &AccountView,
381    allowed: &[Address],
382) -> ProgramResult {
383    let owner = unsafe { account.owner() };
384    let mut i = 0;
385    while i < allowed.len() {
386        if *owner == allowed[i] {
387            return Ok(());
388        }
389        i += 1;
390    }
391    Err(ProgramError::IncorrectProgramId)
392}