Skip to main content

hopper_native/
verify.rs

1//! Verified CPI -- pre/post state assertions around cross-program invocations.
2//!
3//! Every Solana framework fires CPI and blindly trusts the result. If the
4//! called program has a bug, state corruption propagates silently. Hopper
5//! is the first framework to provide substrate-level CPI verification.
6//!
7//! The pattern: snapshot relevant state before CPI, invoke, then assert
8//! post-conditions. If the assertion fails, the instruction aborts before
9//! the corrupted state can be read by downstream logic.
10//!
11//! # Usage
12//!
13//! ```ignore
14//! use hopper_native::verify::{LamportSnapshot, verify_transfer};
15//!
16//! // Before CPI transfer:
17//! let snap = LamportSnapshot::capture(source, destination);
18//!
19//! // Do the CPI:
20//! system_transfer(&source, &destination, amount)?;
21//!
22//! // Verify the transfer actually happened correctly:
23//! snap.verify_transfer(source, destination, amount)?;
24//! ```
25//!
26//! This catches:
27//! - Called program transferring wrong amount
28//! - Called program not deducting from source
29//! - Called program crediting wrong destination
30//! - Integer overflow in lamport accounting
31
32use crate::account_view::AccountView;
33use crate::error::ProgramError;
34use crate::ProgramResult;
35
36// ---- Lamport snapshot ------------------------------------------------
37
38/// Snapshot of lamport balances for two accounts before a CPI.
39///
40/// Captures the source and destination balances so that after the CPI
41/// completes, we can verify the expected transfer occurred.
42#[derive(Clone, Copy, Debug)]
43pub struct LamportSnapshot {
44    source_before: u64,
45    destination_before: u64,
46}
47
48impl LamportSnapshot {
49    /// Capture the current lamport balances of source and destination.
50    #[inline(always)]
51    pub fn capture(source: &AccountView, destination: &AccountView) -> Self {
52        Self {
53            source_before: source.lamports(),
54            destination_before: destination.lamports(),
55        }
56    }
57
58    /// Verify that exactly `amount` lamports moved from source to destination.
59    ///
60    /// Checks:
61    /// 1. Source decreased by exactly `amount`
62    /// 2. Destination increased by exactly `amount`
63    /// 3. No overflow/underflow occurred
64    #[inline]
65    pub fn verify_transfer(
66        &self,
67        source: &AccountView,
68        destination: &AccountView,
69        amount: u64,
70    ) -> ProgramResult {
71        let source_after = source.lamports();
72        let dest_after = destination.lamports();
73
74        // Source must have decreased by exactly `amount`.
75        let source_delta = self
76            .source_before
77            .checked_sub(source_after)
78            .ok_or(ProgramError::ArithmeticOverflow)?;
79        if source_delta != amount {
80            return Err(ProgramError::InvalidAccountData);
81        }
82
83        // Destination must have increased by exactly `amount`.
84        let dest_delta = dest_after
85            .checked_sub(self.destination_before)
86            .ok_or(ProgramError::ArithmeticOverflow)?;
87        if dest_delta != amount {
88            return Err(ProgramError::InvalidAccountData);
89        }
90
91        Ok(())
92    }
93
94    /// Verify that the source decreased by exactly `amount` (one-sided check).
95    ///
96    /// Use this when the destination is a program-controlled escrow or
97    /// fee account where you only care about the deduction.
98    #[inline]
99    pub fn verify_deduction(&self, source: &AccountView, amount: u64) -> ProgramResult {
100        let delta = self
101            .source_before
102            .checked_sub(source.lamports())
103            .ok_or(ProgramError::ArithmeticOverflow)?;
104        if delta != amount {
105            return Err(ProgramError::InvalidAccountData);
106        }
107        Ok(())
108    }
109
110    /// Verify that neither balance changed (no-op CPI or read-only call).
111    #[inline]
112    pub fn verify_unchanged(
113        &self,
114        source: &AccountView,
115        destination: &AccountView,
116    ) -> ProgramResult {
117        if source.lamports() != self.source_before
118            || destination.lamports() != self.destination_before
119        {
120            return Err(ProgramError::InvalidAccountData);
121        }
122        Ok(())
123    }
124
125    /// Get the pre-CPI source balance.
126    #[inline(always)]
127    pub fn source_before(&self) -> u64 {
128        self.source_before
129    }
130
131    /// Get the pre-CPI destination balance.
132    #[inline(always)]
133    pub fn destination_before(&self) -> u64 {
134        self.destination_before
135    }
136}
137
138// ---- Single-account snapshot -----------------------------------------
139
140/// Snapshot of a single account's lamports for simple balance assertions.
141#[derive(Clone, Copy, Debug)]
142pub struct BalanceSnapshot {
143    before: u64,
144}
145
146impl BalanceSnapshot {
147    /// Capture a single account's lamport balance.
148    #[inline(always)]
149    pub fn capture(account: &AccountView) -> Self {
150        Self {
151            before: account.lamports(),
152        }
153    }
154
155    /// Verify the balance increased by at least `min_increase`.
156    #[inline]
157    pub fn verify_increased_by(&self, account: &AccountView, min_increase: u64) -> ProgramResult {
158        let current = account.lamports();
159        let delta = current
160            .checked_sub(self.before)
161            .ok_or(ProgramError::ArithmeticOverflow)?;
162        if delta < min_increase {
163            return Err(ProgramError::InsufficientFunds);
164        }
165        Ok(())
166    }
167
168    /// Verify the balance decreased by at most `max_decrease`.
169    #[inline]
170    pub fn verify_decreased_by_at_most(
171        &self,
172        account: &AccountView,
173        max_decrease: u64,
174    ) -> ProgramResult {
175        let current = account.lamports();
176        let delta = self
177            .before
178            .checked_sub(current)
179            .ok_or(ProgramError::ArithmeticOverflow)?;
180        if delta > max_decrease {
181            return Err(ProgramError::InsufficientFunds);
182        }
183        Ok(())
184    }
185
186    /// Verify the balance is unchanged.
187    #[inline]
188    pub fn verify_unchanged(&self, account: &AccountView) -> ProgramResult {
189        if account.lamports() != self.before {
190            return Err(ProgramError::InvalidAccountData);
191        }
192        Ok(())
193    }
194
195    /// Get the captured balance.
196    #[inline(always)]
197    pub fn before(&self) -> u64 {
198        self.before
199    }
200
201    /// Compute the net change (positive = gained, negative = lost).
202    ///
203    /// Returns the change as an i128 to avoid overflow.
204    #[inline(always)]
205    pub fn net_change(&self, account: &AccountView) -> i128 {
206        account.lamports() as i128 - self.before as i128
207    }
208}
209
210// ---- Data integrity snapshot -----------------------------------------
211
212/// Fast integrity check for account data using FNV-1a hash.
213///
214/// Use this to detect unexpected data mutations around CPI calls.
215/// Not cryptographically secure -- purely for integrity assertions.
216#[derive(Clone, Copy, Debug)]
217pub struct DataFingerprint {
218    hash: u64,
219    data_len: usize,
220}
221
222impl DataFingerprint {
223    /// Compute a fast fingerprint of the first `len` bytes of account data.
224    ///
225    /// Uses FNV-1a (fast, no dependencies, good collision resistance for
226    /// short inputs). Not suitable for cryptographic purposes.
227    #[inline]
228    pub fn capture(account: &AccountView, len: usize) -> Self {
229        let data_len = account.data_len().min(len);
230        let data_ptr = account.data_ptr_unchecked();
231
232        // FNV-1a hash.
233        let mut hash: u64 = 0xcbf29ce484222325;
234        let mut i = 0;
235        while i < data_len {
236            let byte = unsafe { *data_ptr.add(i) };
237            hash ^= byte as u64;
238            hash = hash.wrapping_mul(0x100000001b3);
239            i += 1;
240        }
241
242        Self { hash, data_len }
243    }
244
245    /// Verify the data has not changed since the snapshot.
246    #[inline]
247    pub fn verify_unchanged(&self, account: &AccountView) -> ProgramResult {
248        let current = Self::capture(account, self.data_len);
249        if current.hash != self.hash || current.data_len != self.data_len {
250            return Err(ProgramError::InvalidAccountData);
251        }
252        Ok(())
253    }
254
255    /// Get the fingerprint hash.
256    #[inline(always)]
257    pub fn hash(&self) -> u64 {
258        self.hash
259    }
260}