Skip to content
hopper
Get started
Safety / unsafe

Unsafe invariants

The audit ledger for raw-pointer boundaries and safety contracts.

Trust Posture

Hopper is a safety-layered framework that deliberately uses unsafe for the operations where Rust's ownership system cannot express the invariants we need (pointer casts onto account byte slices, zero-copy overlays, CPI invocation). Every other layer of the framework (header validation, fingerprint checking, tiered loading, frame borrow tracking, validation graphs) exists to make the unsafe core as small and auditable as possible.

Design commitment: unsafe is never used for convenience. It is used only when a safe alternative would require allocation, serialization, or loss of the zero-copy property that makes Hopper competitive.

Audit scope: the unsafe surface audited by this ledger spans the raw source directories that own Hopper's zero-copy and backend boundary code: crates/hopper-native/src, crates/hopper-runtime/src, crates/hopper-solana/src, and crates/hopper-core/src. Unsafe in those directories is limited to raw Solana/backend ABI crossings, zero-copy overlays, Pod reads/writes, CPI assembly, and bounded in-place collection operations.

Every unsafe block in Hopper, its justification, and the invariants that must hold. Organized by module boundary.

CI also emits a machine-generated unsafe inventory artifact. The workflow .github/workflows/unsafe-safety.yml runs scripts/check-unsafe-safety-comments.py --inventory-out unsafe-inventory.md and uploads the resulting markdown table as the unsafe-inventory artifact for each push and pull request.

Current Unsafe Source Inventory

This inventory is generated from the current source tree by scanning for real non-rustdoc unsafe { blocks and pub unsafe fn identifiers in the audited source directories. Every listed unsafe block must have a nearby // SAFETY: comment, and every public unsafe function must carry a rustdoc # Safety section. The CI helper scripts/check-unsafe-safety-comments.py enforces both requirements.

Source file Unsafe blocks Public unsafe functions
crates/hopper-core/src/abi/field_ref.rs 1 -
crates/hopper-core/src/abi/typed_address.rs 2 -
crates/hopper-core/src/account/cursor.rs 1 -
crates/hopper-core/src/account/pod.rs 6 cast_unchecked, cast_unchecked_mut
crates/hopper-core/src/account/reader.rs 2 -
crates/hopper-core/src/account/registry.rs 8 -
crates/hopper-core/src/account/segment.rs 6 -
crates/hopper-core/src/account/verified.rs 6 -
crates/hopper-core/src/accounts/hopper_account.rs 1 owner
crates/hopper-core/src/accounts/program_account.rs 1 owner
crates/hopper-core/src/accounts/segmented.rs 1 -
crates/hopper-core/src/accounts/unchecked.rs 1 owner
crates/hopper-core/src/check/fast.rs 3 -
crates/hopper-core/src/check/guards.rs 1 -
crates/hopper-core/src/check/mod.rs 4 -
crates/hopper-core/src/collections/fixed_vec.rs 6 -
crates/hopper-core/src/collections/journal.rs 3 -
crates/hopper-core/src/collections/packed_map.rs 4 -
crates/hopper-core/src/collections/ring_buffer.rs 2 -
crates/hopper-core/src/collections/slab.rs 5 -
crates/hopper-core/src/collections/slot_map.rs 3 -
crates/hopper-core/src/collections/sorted_vec.rs 5 -
crates/hopper-core/src/cpi/mod.rs 10 -
crates/hopper-core/src/event/mod.rs 8 -
crates/hopper-core/src/frame/mod.rs 17 segment_mut_unchecked
crates/hopper-core/src/virtual_state/mod.rs 2 -
crates/hopper-native/src/account_view.rs 52 owner, assign, borrow_unchecked, borrow_unchecked_mut, segment_ref_unchecked, segment_mut_unchecked, raw_ref, raw_mut, resize_unchecked, close_unchecked
crates/hopper-native/src/address.rs 1 -
crates/hopper-native/src/batch.rs 1 -
crates/hopper-native/src/borrow.rs 3 from_raw_parts, from_raw_parts
crates/hopper-native/src/budget.rs 3 -
crates/hopper-native/src/cpi.rs 9 invoke_unchecked, invoke_signed_unchecked
crates/hopper-native/src/entrypoint.rs 11 process_entrypoint
crates/hopper-native/src/hash.rs 2 -
crates/hopper-native/src/instruction.rs 5 -
crates/hopper-native/src/introspect.rs 2 -
crates/hopper-native/src/lazy.rs 9 lazy_deserialize
crates/hopper-native/src/lens.rs 13 -
crates/hopper-native/src/log.rs 5 -
crates/hopper-native/src/mem.rs 13 memcpy, memmove, memset, memcmp
crates/hopper-native/src/pda.rs 21 -
crates/hopper-native/src/project.rs 8 project_safe_mut, project_mut, project_hopper_mut
crates/hopper-native/src/raw_input.rs 33 deserialize_accounts, deserialize_accounts_fast, scan_instruction_frame
crates/hopper-native/src/return_data.rs 2 -
crates/hopper-native/src/system.rs 1 -
crates/hopper-native/src/sysvar.rs 3 -
crates/hopper-native/src/token.rs 1 -
crates/hopper-native/src/verify.rs 1 -
crates/hopper-runtime/src/account.rs 26 owner, raw_ref, raw_mut, assign, borrow_unchecked, borrow_unchecked_mut, resize_unchecked, close_unchecked
crates/hopper-runtime/src/account_wrappers.rs 0 new_unchecked, new_unchecked, new_unchecked
crates/hopper-runtime/src/address.rs 1 -
crates/hopper-runtime/src/audit.rs 2 -
crates/hopper-runtime/src/borrow.rs 13 project, project
crates/hopper-runtime/src/borrow_registry.rs 2 -
crates/hopper-runtime/src/compat/mod.rs 1 -
crates/hopper-runtime/src/compat/native.rs 9 wrap_account_slice, account_owner, assign, process_entrypoint
crates/hopper-runtime/src/compat/pinocchio.rs 17 wrap_account_slice, account_owner, assign, process_entrypoint
crates/hopper-runtime/src/compat/solana_program.rs 16 borrow_unchecked, borrow_unchecked_mut, close_unchecked, wrap_account_slice, account_owner, assign, process_entrypoint
crates/hopper-runtime/src/context.rs 4 raw_ref, raw_mut, raw_unchecked, as_mut_ptr
crates/hopper-runtime/src/cpi.rs 12 invoke_unchecked, invoke_signed_unchecked
crates/hopper-runtime/src/dyn_cpi.rs 1 -
crates/hopper-runtime/src/foreign.rs 1 -
crates/hopper-runtime/src/instruction.rs 4 -
crates/hopper-runtime/src/interop.rs 2 -
crates/hopper-runtime/src/layout.rs 3 -
crates/hopper-runtime/src/lib.rs 11 -
crates/hopper-runtime/src/log.rs 4 -
crates/hopper-runtime/src/option_byte.rs 2 -
crates/hopper-runtime/src/pda.rs 7 -
crates/hopper-runtime/src/segment_lease.rs 1 new
crates/hopper-runtime/src/syscall.rs 2 -
crates/hopper-runtime/src/syscalls.rs 7 sol_log_data, sol_sha256
crates/hopper-runtime/src/token.rs 10 -
crates/hopper-solana/src/compute.rs 1 -
crates/hopper-solana/src/crypto/merkle.rs 2 -
crates/hopper-solana/src/mint.rs 2 -
crates/hopper-solana/src/token.rs 2 -

Trust Summary

Hopper's unsafe surface is deliberately narrow and follows three foundational rules:

  1. All overlay targets are alignment-1. No pointer cast in the codebase produces a reference with align > 1. This eliminates alignment UB entirely.
  2. All casts are bounds-checked. Every pod_from_bytes / overlay_at / read_unaligned call is preceded by a length check against T::SIZE or explicit offset arithmetic.
  3. Aliasing is structurally prevented. Mutable borrows flow through &mut self (compile-time) or frame-level bitmask tracking (runtime). No two mutable references can alias the same account data.

What tests prove it

Invariant Test Suite File
Pod boundary rejection 38 tests tests/unsafe_boundary_tests.rs
Overlay checked/unchecked parity 24 tests tests/overlay_equivalence_tests.rs
Compat regression & receipt wire format 26 tests tests/compat_regression_tests.rs
Property-based ABI roundtrip 36 tests tests/property_tests.rs
CPI guard, collections, registry, validation 96 tests tests/trust_tests.rs

What callers must guarantee

API Caller Obligation
unsafe impl Pod for T T is #[repr(C)] or #[repr(transparent)], all fields are [u8; N] or Pod, align_of::<T>() == 1
cast_unchecked / cast_unchecked_mut data.len() >= size_of::<T>(). No concurrent aliasing.
hopper_layout! load_unchecked Account data is valid for the layout. Caller accepts all risk.
MaybeUninit transmute in CPI builders All ACCTS slots initialized via add_account() before invoke()

Global Guarantees

  1. #![deny(unsafe_op_in_unsafe_fn)] -- enforced in hopper-core and hopper-solana. All unsafe operations must be explicitly wrapped even inside unsafe fn.
  2. Pod trait -- unsafe trait Pod: Copy + Sized requires align_of == 1 and all bit patterns valid. Every unsafe impl Pod is for types whose fields are [u8; N] or nested Pod types under #[repr(C)]/#[repr(transparent)].
  3. All pointer casts target align-1 types. No pointer cast in the codebase produces a reference to a type with alignment > 1.

hopper-core::abi

Wire types (integers.rs, boolean.rs)

Line(s) Construct Invariant
unsafe impl WireType Trait impl per wire type Type is #[repr(transparent)] over [u8; N], align == 1, size == N (compile-time asserted). All bit patterns valid.
unsafe impl Pod Trait impl per wire type Same as above.

typed_address.rs

Line Construct Invariant
61 unsafe impl Pod for TypedAddress<T> #[repr(transparent)] over [u8; 32]. PhantomData is ZST. Size == 32, align == 1 (compile-time asserted).
99 &*(account.address() as *const Address as *const [u8; 32]) hopper_native::Address is [u8; 32] (same repr). Read-only, no-alloc.
198 unsafe impl Pod for UntypedAddress #[repr(transparent)] over [u8; 32].

field_ref.rs

Line Construct Invariant
88 &*(self.data.as_ptr() as *const [u8; 32]) Slice length checked ≥ 32 before cast. Target type is [u8; 32], align 1.

hopper-core::account

pod.rs

Line Construct Invariant
13 pub unsafe trait Pod Marker trait. Implementors guarantee align-1, all bit patterns valid.
16-17 unsafe impl Pod for u8 / [u8; 32] Trivially safe.
39 pod_from_bytes: &*(data.as_ptr() as *const T) Size checked: data.len() >= T::SIZE. T: Pod guarantees align-1. No aliasing: immutable borrow.
54 pod_from_bytes_mut: &mut *(data.as_mut_ptr() as *mut T) Size checked. T: Pod. Caller must ensure exclusive access.
64 pod_read: read_unaligned Size checked. T: Pod. Returns by value, no alias concern.
74 pod_write: write_unaligned Size checked. T: Pod. Caller must hold &mut [u8].

header.rs

Line Construct Invariant
49 unsafe impl Pod for AccountHeader #[repr(C)] of all byte-array fields. Size == 16, align == 1 (asserted).

verified.rs

Line Construct Invariant
36 VerifiedAccount::get(): &*(data.as_ptr() as *const T) Size validated at construction (data.len() >= T::SIZE). T: Pod. Immutable.
99 overlay_at: &*(data.as_ptr().add(offset) as *const U) Bounds checked: offset + U::SIZE <= data.len(). U: Pod.
126 VerifiedAccountMut::get() Same as VerifiedAccount::get().
133 get_mut(): &mut *(data.as_mut_ptr() as *mut T) Size validated. Exclusive access via &mut self.
180 overlay_at (mut variant) Bounds checked.
190 overlay_at_mut Bounds checked. Exclusive access via &mut self.

VerifiedAccount and VerifiedAccountMut are proof wrappers, not ordinary raw account accessors. They may return &T / &mut T, &[u8], &mut [u8], or secondary overlays, but every returned reference is tied to &self or &mut self. The wrapper itself owns either the Hopper borrow guard or a pre-validated raw slice, so the reference cannot outlive the proof object and cannot be held after the guard is dropped. This is intentionally distinct from the HopperRefOnly field-access path, where naked raw references are rejected at the macro boundary.

The compile-fail fixture tests/hopper-trybuild/tests/ui/fail/verified_ref_outlives_wrapper.rs locks in this lifetime boundary: attempting to return a VerifiedAccount::get() result after the wrapper goes out of scope fails to compile.

reader.rs

Line Construct Invariant
42 Header overlay cast Data length checked ≥ HEADER_LEN at construction.
114 Address overlay at offset Bounds checked.

segment.rs

Line Construct Invariant
37 unsafe impl Pod for SegmentDescriptor #[repr(C)], all byte fields.
147, 175, 246, 257, 310, 321 Pointer offset casts All bounds-checked before cast. Target types are Pod (align-1).

registry.rs

Line Construct Invariant
226, 236, 268, 320, 418, 439, 449, 489 Pointer offset casts All preceded by bounds checks against self.data.len(). Target types: SegmentEntry (Pod, align-1), or generic T: Pod.

cursor.rs

Line Construct Invariant
135 Address cast at cursor position Position + 32 <= data.len() checked.

lifecycle.rs

No raw unsafe blocks. Uses hopper_runtime::AccountView safe APIs.


hopper-core::check

mod.rs

Line Construct Invariant
142 keys_eq_fast: read_unaligned x 4 Input is &[u8; 32], always valid for u64 reads at offsets 0/8/16/24.
159 is_zero_address: read_unaligned x 4 Same as above.
179 Address cast in check_has_one hopper_native::Address is [u8; 32].
239 borrow_unchecked() in check_account Immutable borrow for validation only. No conflicting mutable borrows at this point (called before execution phase).
353 borrow_unchecked() in check_discriminator (via macro) Same pattern.
405 account.owner() in check_owner_multi AccountView's unsafe owner() reads the owner field. No alias concern (read-only).

fast.rs

Line Construct Invariant
71-82 read_account_header Reads first 4 bytes of RuntimeAccount via pointer dereference. Relies on AccountView being #[repr(C)] with first field = pointer to RuntimeAccount base. Gated to target_os = "solana" only. Preconditions: SVM guarantees valid input buffer layout.
103 Call to read_account_header Within check_account_fast, called on SVM-provided AccountView.

modifier.rs

Line Construct Invariant
160 borrow_unchecked() in Account<T>::from_account Owner check passed. Frame-level borrow tracking prevents conflicting mutable borrows.
179 borrow_unchecked_mut() in AccountMut<T>::from_account Owner + writable checks passed. Caller ensures exclusive access at frame level.

hopper-core::cpi

mod.rs

Line Construct Invariant
58, 207 MaybeUninit::uninit().assume_init() (array of MaybeUninit) Creating an array of MaybeUninit<&AccountView> from uninit is sound: MaybeUninit<T> does not require initialization. Added slots are initialized via add_account before invoke.
76, 224 Address cast from AccountView::address() Address is [u8; 32]. Read-only cast.
122, 260 View transmute from MaybeUninit array All ACCTS slots initialized via add_account (enforced by debug_assert_eq!(acct_cursor, ACCTS)). The transmute from [MaybeUninit<&T>; N] to [&T; N] is sound when all N elements are initialized.
128, 265 core::mem::zeroed() for InstructionAccount array InstructionAccount has no invalid bit patterns (contains &[u8; 32] pointer + 2 bools). Zeroed pointers are overwritten before use.
150, 154, 285, 287 core::mem::zeroed() for Signer/Seed buffers Same pattern. All used slots are written before invoke_signed_unchecked.

hopper-core::collections

All collections follow the same pattern: bounds-checked pointer arithmetic on &[u8] / &mut [u8] slices, with target types that are Pod (align-1).

Module Pattern Invariant
fixed_vec read_unaligned, overlay casts Count/capacity validated. Offset arithmetic checked against data.len().
ring_buffer write_unaligned, overlay casts Head/count maintained modulo capacity. Offsets checked.
slot_map Overlay casts with generation counter Slot index validated.
bit_set None (all byte-level) N/A
sorted_vec read_unaligned, write_unaligned, copy_within Count validated, offsets checked. copy_within uses ptr::copy for overlapping regions.
journal write_unaligned, read_unaligned Cursor wraps within capacity. Bounds checked.
slab Offset casts, read_unaligned Bitmap allocation tracking. Slot offset validated against data length.
packed_map read_unaligned, write_unaligned Count validated, entry size arithmetic checked.

hopper-core::frame

phase.rs

Line Construct Invariant
borrow_mut borrow_unchecked_mut() via ExecutionContext Runtime borrow tracking via u64 bitmask (mutable_borrows). Each bit corresponds to an account index. AccountBorrowFailed returned on double-mutable-borrow.
borrow borrow_unchecked() Immutable borrow. No conflict tracking needed (follows Rust's shared-borrow model).

hopper-macros

hopper_layout!

Construct Invariant
unsafe impl Pod for $name Generated struct is #[repr(C)] over alignment-1 fields. Compile-time assertions enforce size_of == LEN and align_of == 1.
borrow_unchecked() / borrow_unchecked_mut() in load functions Protected by tiered validation: T1 checks owner + disc + version + layout_id + size before borrow. T2 checks owner + layout_id + size.
load_unchecked Explicitly marked unsafe fn. Caller assumes all risk.
load_unverified Size checked. Returns overlay even without full validation (tier 5 for indexers).

hopper_check!

Construct Invariant
borrow_unchecked() in disc/size arms Immutable borrow for validation reads. Called during resolve/validate phase (before any mutable borrows).

hopper-solana

token.rs, mint.rs

Line Construct Invariant
All pointer casts Data length >= TOKEN_ACCOUNT_LEN or MINT_LEN checked before cast. Target: Address (align 1).

cpi_guard.rs

Line Construct Invariant
71 instructions_sysvar.borrow_unchecked() Used to read the Instructions sysvar. Immutable, read-only.

typed_cpi.rs

Line Construct Invariant
298-299 borrow_unchecked() in checked_token_transfer Read-only borrows to compare mint fields before CPI. No conflicting mutable access at this point.

Audit Checklist

For any new unsafe added to the codebase, verify:

  • Bounds check precedes every pointer offset/cast
  • Target type is Pod (align-1, all bits valid)
  • // SAFETY: comment present and accurate
  • Mutable borrows tracked by frame bitmask or exclusive &mut access
  • No UB on the off-chain (non-SVM) path
  • target_os = "solana" gate if relying on SVM runtime layout

Unsafe Review Checklist (for auditors)

When reviewing Hopper code (or code that depends on Hopper), walk through these questions for every unsafe block:

  1. Is the target type alignment-1? Every Pod type in Hopper is #[repr(C)] or #[repr(transparent)] with all fields being [u8; N] or nested Pod types. If a new type is introduced, verify align_of == 1 with a compile-time assertion.

  2. Is the slice length checked before the cast? Every pod_from_bytes, overlay_at, and manual pointer cast must be preceded by data.len() >= T::SIZE or equivalent bounds arithmetic.

  3. Is aliasing structurally prevented? Mutable access must flow through either &mut self (compile-time) or the frame-level borrow bitmask (runtime). No two mutable references should be able to alias the same account data within a single instruction.

  4. Does it work off-chain? Code gated to target_os = "solana" may assume SVM account layout. Verify that the non-SVM path either provides equivalent safety or is unreachable.

  5. Is the // SAFETY: comment accurate and complete? It must state the precondition, why it holds, and what would go wrong if it didn't.

  6. Are MaybeUninit uses fully initialized before read? CPI builders use MaybeUninit arrays. Verify that add_account() is called for every slot before invoke().

  7. Does the test suite cover the boundary? Each unsafe boundary should have at least one test that exercises the happy path and one that exercises the rejection path (wrong size, wrong alignment, etc.).


Test Coverage by Danger Zone

Every module with unsafe blocks has corresponding tests that exercise the invariant boundaries. This table maps each risk area to its test coverage.

Module Risk Key Invariant Test Coverage
abi::integers Wire type soundness align == 1, size == WIRE_SIZE Compile-time assertions + prop_wire_* property tests
abi::typed_address Address cast soundness Address is [u8; 32], read-only prop_typed_address_* property tests
abi::fingerprint Deterministic hashing SHA-256 prefix must change with schema fingerprint_* golden tests in trust_tests
account::pod Overlay cast bounds data.len() >= T::SIZE before cast prop_pod_* + compile-time size_of assertions
account::segment Segment offset math Bounds checked before every cast segment_* trust tests + property tests
account::registry Registry pointer offset All offsets validated against data.len() registry_* trust tests
check::mod Sysvar instruction parsing Offset table + per-ix layout fidelity cpi_guard_* + sysvar_parse_* golden tests (with 0/1/N account metas)
check::fast RuntimeAccount header read SVM-only, gated to target_os = "solana" Relies on SVM runtime guarantees; untestable off-chain
cpi::mod MaybeUninit transmute All ACCTS slots initialized before transmute debug_assert_eq!(acct_cursor, ACCTS) + off-chain no-op path
cpi::mod CPI builder zeroed data InstructionAccount overwritten before invoke Off-chain path returns Ok(()), SVM path exercises full path
collections::journal Circular wrap + copy_nonoverlapping Head wraps within capacity, bounds checked journal_* trust tests: strict/circular, wrap-many, ordering, latest, out-of-bounds
collections::slab Bitmap + offset arithmetic Slot index validated, bounds checked slab_* trust tests: alloc/free cycle, double-free reject, full/realloc
collections::fixed_vec read_unaligned overlay Count/capacity validated fixed_vec_* unit tests
collections::ring_buffer write_unaligned overlay Head/count modulo capacity ring_buffer_* unit tests
collections::sorted_vec ptr::copy for insert/remove Count validated, offsets checked sorted_vec_* trust + property tests
frame::phase Borrow tracking bitmask u64 bitmask prevents double-mutable-borrow frame_* property tests
hopper-macros hopper_layout! Pod derivation Compile-time size_of == LEN, align_of == 1 Every generated type gets static assertions; used in all test layouts
hopper-solana::token Token account overlay data.len() >= TOKEN_ACCOUNT_LEN checked token_* integration tests
hopper-solana::cpi_guard Instructions sysvar borrow Immutable read for validation cpi_guard_* trust tests (12 tests covering all guard variants)
receipt Fingerprint hashing of account data FNV-1a deterministic, before/after tracked receipt_* trust tests (12 tests) + prop_receipt_* property tests (9 tests)

Boundary Test Files

The following dedicated test files exercise unsafe boundaries directly:

  • tests/unsafe_boundary_tests.rs - Pod from undersized/empty/oversized buffers, VerifiedAccount rejection, overlay-at OOB rejection, usize::MAX overflow check, header wire layout verification, segment descriptor boundary conditions, wire type roundtrips, unchecked cast parity.
  • tests/overlay_equivalence_tests.rs - pod_from_bytes vs pod_read value equivalence, VerifiedAccount::get() vs raw pod parity, overlay_at vs manual slice pod parity, cast_unchecked vs checked parity, mutable write-through equivalence, wire type overlay vs raw bytes, header overlay vs constructor.

Hopper Safety Audit Response (2026-04)

The independent Hopper Safety Audit (see docs/Hopper Safety Audit.docx) flagged four specific unsound or permissive surfaces. This section records the action taken on each finding and the invariants the fix now enforces.

Finding 1. hopper-core::frame::{segment_ref, segment_mut, segment_mut_unchecked} returned naked references to T after dropping the backing byte-slice borrow

Fix landed: crates/hopper-core/src/frame/mod.rs now returns hopper_runtime::Ref<'_, T> / RefMut<'_, T> projected through the live byte-slice guard via Ref::project / RefMut::project. The returned guard owns the account's borrow state byte. it is released only when the typed reference drops, not when the function returns.

Invariant enforced: borrow state byte always matches the set of live Ref<T> / RefMut<T> guards returned from Frame.

Regression tests: frame::audit_tests::frame_segment_mut_writes_through_ref_mut, frame::audit_tests::frame_segment_ref_returns_live_guard.

Finding 2. T: Copy bound on hot-path access was too loose

bool, char, &T, NonZeroU64, and padded #[repr(C)] structs all satisfy Copy but are not safe to overlay on arbitrary bytes.

Fix landed: the canonical Pod trait now lives at the substrate layer (hopper_native::Pod) with the contract documented as four explicit obligations (all bit patterns valid, alignment-1, no padding, no interior pointers). hopper-runtime re-exports it (hopper_runtime::Pod) when the native backend is active; hopper-core re-exports it as hopper_core::account::Pod. Every hot-path access API tightened from T: Copy to T: Pod:

  • hopper_native::AccountView::{segment_ref, segment_mut, segment_ref_unchecked, segment_mut_unchecked, raw_ref, raw_mut}
  • hopper_runtime::AccountView::{segment_ref, segment_mut, segment_ref_const, segment_mut_const, segment_ref_typed, segment_mut_typed, raw_ref, raw_mut}
  • hopper_runtime::Context::{segment_ref, segment_mut, segment_ref_const, segment_mut_const, segment_ref_typed, segment_mut_typed, raw_ref, raw_mut, raw_unchecked, read_data}
  • hopper_core::frame::Frame::{segment_ref, segment_mut, segment_mut_unchecked}
  • Macro-generated __SegT escapes from #[hopper::context]

Finding 3. Projectable trait too permissive (Copy + 'static)

Fix landed: crates/hopper-native/src/project.rs now documents Projectable as the Tier-C unsafe escape hatch kept for backward compatibility with already-published programs. A strengthened SafeProjectable trait + project_safe / project_safe_mut helpers reject zero-sized overlays at compile time and steer all new code toward the Pod-bounded path. hopper-native::lens::read_field_pod added as a drop-in Pod-bounded replacement for read_field.

Finding 4. CLI/IDL/DX gaps

Fix landed: #[hopper::pod] standalone attribute macro (crates/hopper-macros-proc/src/pod.rs) lets any #[repr(C)] struct opt into the full contract without the #[hopper::state] header/layout_id/schema machinery. CLI already has hopper compile --emit rust, hopper inspect, hopper explain, hopper client gen --ts, hopper client gen --kt, hopper manager … wiring (see tools/hopper-cli).

New compile-time fences (Quasar-inspired hardening)

#[hopper::state] now emits three additional const _: () = assert!(...) fences on every generated layout:

Fence What it catches
align_of::<T>() == 1 #[repr(C)] struct with a non-alignment-1 field slipped in (e.g. raw u64 instead of WireU64)
size_of::<T>() == sum of field sizes Compiler-inserted padding between fields
size_of::<T>() > 0 Zero-sized overlay (projects to a dangling pointer)
DISC != 0 Zero discriminator cannot be distinguished from an uninitialized buffer

All four fire at type-check time. Malformed layouts never reach link.

Const-generic TypedSegment<T, const OFFSET: u32> (audit innovation item)

New crates/hopper-runtime/src/segment.rs introduces a zero-sized TypedSegment marker that bakes both the overlay type and the body offset into the type system. AccountView::segment_ref_typed / segment_mut_typed and Context::segment_ref_typed / segment_mut_typed take such a marker and lower to ptr + literal_offset SBF with a literal size bounds check. A compile-time const _: () proves the marker is zero-sized so passing it around is free.

Coordination with live borrow state (audit appendix)

Three Hopper-runtime regression tests lock in the cross-path coordination the audit wanted proven:

  • live_load_blocks_segment_mut. account.load::<T>() + subsequent segment_mut rejected via the native state byte.
  • live_load_mut_blocks_segment_ref. exclusive load_mut rejects a concurrent segment_ref even though they use different registries.
  • every_access_path_is_tracked. walks every safe access method and asserts each one blocks a conflicting follow-up.

These, together with the Frame audit regression tests, mean every safe access path in Hopper is now covered by at least one regression test that fails loudly if the coordination breaks.

  • tests/compat_regression_tests.rs - Append-safe addition detection, forbidden field rename/resize, field removal as breaking, compare_fields report accuracy, is_backward_readable / requires_migration correctness, receipt wire format encode/decode roundtrip, Phase/CompatImpact enum roundtrips, segment/field mask roundtrip, reserved byte verification.

Post-Audit Closure

Last focused verification refresh: 2026-05-25. Current proof lanes are listed in the Verification section; avoid treating this document as a frozen workspace-wide test-count snapshot.

This section enumerates every item in the docs/Hopper Safety Audit.docx and points at the source-of-truth closure in the current tree. It is the ground truth the audit will be compared against on re-review.

Must-fix (5 of 5. DONE)

# Audit item Closure
M1 Reject malformed duplicate-account indices crates/hopper-native/src/raw_input.rs:16-46, 112-114, 176-179 (malformed_duplicate_marker trap on any forward/self-ref); lazy.rs:231-233 mirrors for lazy parse; D3 fuzz target continuously adversarial-tests the invariant
M2 RAII segment leases crates/hopper-runtime/src/segment_lease.rs (SegmentLease/SegRef/SegRefMut with Drop); integrated into Frame::segment_ref/segment_mut at crates/hopper-core/src/frame/mod.rs:207-300; regression tests in trust_tests.rs
M3 Canonical wire-fingerprint layout identity crates/hopper-macros-proc/src/state.rs:373-467. canonical_wire_stem + hopper:wire:v2 descriptor, SHA-256-hashed; spelling-drift regression tests at state.rs:515-568
M4 Field-level Pod proof at macro expansion crates/hopper-macros-proc/src/pod.rs and src/state.rs now emit a __FieldPodProof<T: bytemuck::Pod + Zeroable> marker per field. a bare unsafe impl bytemuck::Pod is a rubber stamp that does not check fields, so this closes the hole rubber stamps left. Every field type is forced through the trait bound at expansion time
M5 Compile-fail doctests for negative proof crates/hopper-runtime/src/pod.rs carries 3 compile-fail doctests for non-Pod overlays; the root tests/ui.rs suite has 17 compile-fail fixtures under tests/compile_fail/; tests/hopper-trybuild/tests/ui/ adds 6 generated macro-DX fixtures covering minimal state expansion, Pod alignment accept/reject, #[state] Copy enforcement, #[program] crank handler arity, and verified-ref lifetime rejection

Should-fix (4 of 4. DONE)

# Audit item Closure
S1 Address fingerprint collision safety crates/hopper-runtime/src/segment_borrow.rs:45-67. fast-path 8-byte compare + full-address fallback at line 197, 212-213
S2 Retire Projectable/SafeProjectable split crates/hopper-native/Cargo.toml legacy-projectable feature; SafeProjectable marked Tier-C; ZeroCopy is the unified modern surface
S3 Tighten close / close_to preconditions crates/hopper-runtime/src/account.rs:764-783 (writable + owner + dest-writable checks); crates/hopper-native/src/account_view.rs:389-415 (System Program ID constant + doc clarity)
S4 Fix stale T: Copy docs where code requires T: Pod Audited across crates/; all zero-copy signature docs now say T: Pod (see crates/hopper-runtime/src/context.rs:229-244, account.rs:182-187)

Structural (3 of 4 DONE; 1 deferred with rationale below)

# Audit item Status
ST1 Unify trait model -> ZeroCopy -> WireLayout -> AccountLayout DONE. crates/hopper-runtime/src/zerocopy.rs defines the three-tier stack; blanket impls make every LayoutContract automatically an AccountLayout
ST2 Anchor-grade declarative account constraints DONE (parser + validation + lifecycle). crates/hopper-macros-proc/src/context.rs now parses init/zero/close/realloc/realloc_payer/realloc_zero/payer/space/seeds/bump/has_one/owner/address/constraint; emits ordered validation per audit page 12; generates init_{field}/close_{field}/realloc_{field} lifecycle helpers. Deferred: typed wrappers Signer<'info>/Account<T> (attribute-directed lowering is functionally equivalent today)
ST3 Schema epoch in header + wire fingerprinting DONE. HopperHeader::schema_epoch: u32 at bytes 12-15; AccountLayout::WIRE_FINGERPRINT: u64 constant
ST4 hopper compile beyond --emit rust DONE. hopper compile --emit routes Rust preview, TypeScript, Kotlin, Python, Go, C, Rust client, IDL, Codama, and schema output through the shared manifest-source path

DX (1 of 4 DONE; 3 documented)

# Audit item Status
DX1 End-to-end build/test/deploy CLI DONE. tools/hopper-cli/src/cmd/lifecycle.rs:86-246
DX2 Cleaner generated access surfaces DONE. #[hopper::state] now emits {FIELD}_ABS_OFFSET: u32 inherent consts that fold HEADER_LEN + offset; callers pass them to typed-segment escapes directly without arithmetic boilerplate. Regression tests at examples/hopper-proc-vault/src/lib.rs:abs_offset_tests
DX3 Authored-language compile pipeline end-to-end DEFERRED. requires ST4 plus manifest->IDL->client unification; orthogonal to the safety audit
DX4 Canonical account/context syntax with full PDA/init/realloc/close/payer/space DONE via ST2 closure. every audit-listed attribute now lowers through #[hopper::context]

Docs and tests (2 of 4 DONE; 2 documented)

# Audit item Status
D1 Canonical unsafe-invariants document DONE. this file
D2 Compile-fail coverage DONE. The current UI proof lanes are cargo test --test ui --features proc-macros --locked for 17 root compile-fail fixtures, plus cargo test -p hopper-trybuild --locked for 2 pass fixtures and 4 compile-fail fixtures under tests/hopper-trybuild/tests/ui/. Together they pin Pod validity, state constraints, dynamic tails, tiny profile restrictions, borrow guards, generated state/program diagnostics, and prelude ergonomics.
D3 Fuzzing low-level loaders/parsers DONE. fuzz/ crate with 4 targets (fuzz_instruction_frame, fuzz_decode_header, fuzz_decode_segments, fuzz_pod_overlay) + new safe bounds-checked parser parse_instruction_frame_checked in raw_input.rs with 7 regression tests
D4 Benchmark suite across frameworks DONE as a sibling product. The hopper-bench repo owns primitive benchmarks, cross-framework parity vaults, competitor locks, raw logs, and CI thresholds; this framework repo keeps release docs and lightweight result snapshots only.

Innovations (5 of 5 DONE)

# Audit innovation Status
I1 Borrow stack with typed leases DONE. SegmentLease / SegRef / SegRefMut RAII stack in crates/hopper-runtime/src/segment_lease.rs
I2 Generated typed-segment tokens everywhere DONE. {FIELD}_OFFSET, {FIELD}_ABS_OFFSET, {FIELD}_SIZE, {FIELD}_TYPE const emission from #[hopper::state]; #[hopper::context] consumes both
I3 Manifest-backed foreign account lenses DONE. crates/hopper-runtime/src/foreign.rs. ForeignManifest + ForeignLens<T> with four-step verification (owner / disc / wire_fp / schema_epoch range)
I4 Schema epoch with in-place migration helpers DONE. #[hopper::migrate(from, to)] proc macro in crates/hopper-macros-proc/src/migrate.rs + hopper::layout_migrations! composition macro + apply_pending_migrations runtime in crates/hopper-runtime/src/migrate.rs. 8 integration tests in tests/migrate_integration.rs
I5 Hybrid serialization (fixed body + typed dynamic tail) DONE. #[hopper::state(dynamic_tail = T)] + TailCodec trait (Borsh-subset) in crates/hopper-runtime/src/tail.rs. 12 codec + 8 integration tests

Follow-up design closure

On top of the original audit, a follow-up design pass called for the Jiminy-replacement safety surface, the hopper verify ABI-integrity command, and client-side layout verification. All three are now in-tree:

Design item Closure
require! / require_eq! crates/hopper-runtime/src/lib.rs
require_neq! crates/hopper-runtime/src/lib.rs
require_keys_eq! / require_keys_neq! (Jiminy-familiar) crates/hopper-runtime/src/lib.rs
require_gte! / require_gt! crates/hopper-runtime/src/lib.rs
check_signer / check_owner / check_writable free fns crates/hopper-core/src/check/mod.rs (pre-existing)
check_program(account, program_id) free fn crates/hopper-core/src/check/mod.rs (added post-audit)
checked_mul_div / checked_mul_div_ceil safe math crates/hopper-core/src/math/mod.rs (pre-existing)
hopper verify CLI tools/hopper-cli/src/cmd/verify.rs (manifest integrity + binary scan)
#[used] LAYOUT_ID anchor crates/hopper-macros-proc/src/state.rs emits per-layout static
Client-side assertLayoutId(data, hex) crates/hopper-schema/src/clientgen.rs TS generator
Per-layout assert{Name}Layout(data) helpers Same generator, paired with {NAME}_LAYOUT_ID const
hopper init scaffold tools/hopper-cli/src/cmd/lifecycle.rs::cmd_init (pre-existing)
Rust off-chain client generator crates/hopper-schema/src/rust_client.rs - RsClientGen emits ClientError, assert_{name}_layout, decode_{name}, {Ix}_ix builders; wired as hopper compile --emit rust-client; 9 regression tests
Token CPI signer-pre-check default crates/hopper-runtime/src/token.rs - Transfer/MintTo/Burn/CloseAccount/Approve/Revoke invoke() runs require_authority_signed_direct before the CPI so a missing signer surfaces MissingRequiredSignature instead of an opaque CPI error
SPL *Checked builders (Token-2022 extension safety) crates/hopper-runtime/src/token.rs - TransferChecked (idx 12), ApproveChecked (idx 13), MintToChecked (idx 14), BurnChecked (idx 15) carry a decimals: u8 byte the SPL token program validates against the mint's stored decimals. Every invoke() applies the same signer pre-check. 7 wire-format regression tests lock the byte layout
Deprecation of unchecked token builders crates/hopper-runtime/src/token.rs + crates/hopper-solana/src/typed_cpi.rs - Transfer, MintTo, Burn, Approve structs and their token_* wrapper functions marked #[deprecated(note = "use *Checked for Token-2022 safety")]. New token_transfer_checked, token_transfer_checked_signed, token_mint_to_checked, token_mint_to_checked_signed, token_burn_checked, token_burn_checked_signed, token_approve_checked free functions expose the safe path in hopper-solana
Validation auto-injection (audit final-API Step 7) crates/hopper-macros-proc/src/program.rs:184 - every #[hopper::program] dispatcher emits <ContextSpec>::bind(ctx)?, which runs the context's validate(ctx)? before handing the bound context to the user's handler body. Users cannot reach the handler without the full signer/mut/owner/address/PDA/layout/has_one/constraint gauntlet passing first
Sealed ZeroCopy trait (audit final-API Step 5) crates/hopper-runtime/src/zerocopy.rs::__sealed::HopperZeroCopySealed gates the blanket ZeroCopy impl. Every Hopper-authored surface stamps the seal: #[hopper::pod], #[hopper::state], hopper_layout! (both forms), and every primitive wire type (Wire{U,I}{8,16,32,64,128}, WireBool, TypedAddress<T>, UntypedAddress, AccountHeader, SegmentDescriptor, Address, plus the u8/u64/[u8; N] framework primitives). A user bypassing the macros with a bare unsafe impl Pod for Foo does not pick up ZeroCopy automatically. They would have to explicitly name the doc-hidden __sealed::HopperZeroCopySealed path, which signals the opt-out is deliberate
Canonical segment-access path documented crates/hopper-runtime/src/context.rs - rustdoc table on Context::segment_ref points callers to segment_ref_typed as the compile-time-offset canonical; runtime-offset variant stays available for dynamic iteration
Compile-proven borrow-guard constraint (audit Finding 2) crates/hopper-runtime/src/ref_only.rs::HopperRefOnly is a sealed marker trait implemented only by Ref, RefMut, SegRef, SegRefMut. An API bounded by G: HopperRefOnly rejects naked &T / &mut T at compile time. The seal lives in a private sealed module so downstream crates cannot stamp the marker onto arbitrary types. Closure test: tests/compile_fail/ref_only_rejects_raw_ref.rs captures rustc's error[E0277]: the trait bound '&mut u64: HopperRefOnly' is not satisfied verbatim
Policy-driven zero-copy runtime (strict / sealed / raw) crates/hopper-runtime/src/policy.rs::HopperProgramPolicy ships three named modes (STRICT, SEALED, RAW) plus custom lever-by-lever overrides. #[hopper::program(strict)] / (raw) / (sealed) / (allow_unsafe = false, ...) parses the attribute in crates/hopper-macros-proc/src/program.rs::parse_program_policy and emits pub const HOPPER_PROGRAM_POLICY: HopperProgramPolicy = ...; inside the module. When allow_unsafe = false, every handler gets #[deny(unsafe_code)] unless it opts back in via #[instruction(N, unsafe_memory)], per-handler const <HANDLER>_POLICY: HopperInstructionPolicy captures the override. Three-mode demonstration: examples/hopper-policy-vault/src/lib.rs with compile-time assert! blocks that lock the emitted constants to the named policies
Canonical raw-pointer escape hatch on Context crates/hopper-runtime/src/context.rs::Context::as_mut_ptr and as_ptr expose the explicit unsafe fn / safe-to-obtain pointer primitive the audit names. as_mut_ptr requires require_writable; as_ptr requires check_borrow. Both yield pointers into the loader-provided per-account buffer, transferring alias-safety to the caller as documented. Exercised by examples/hopper-policy-vault::raw_vault::raw_pointer_reset, which writes directly to a field at a compile-computed offset in raw mode
Token-authority ownership pre-check (enforce_token_checks) crates/hopper-runtime/src/token.rs::require_token_authority verifies the SPL TokenAccount's owner field (bytes [32..64]) matches the authority's address before any CPI. Wired into TransferChecked::invoke_strict, TransferChecked::invoke_signed_strict, BurnChecked::invoke_strict, BurnChecked::invoke_signed_strict, ApproveChecked::invoke_strict, ApproveChecked::invoke_signed_strict. Handlers inside #[hopper::program(enforce_token_checks = true)] use the *_strict variants to get the attacker-passes-correct-pubkey-but-wrong-signer exploit class closed with a ProgramError::IncorrectAuthority before the CPI instead of an opaque SPL failure. Three regression tests in token::tests pin the accept/reject/short-buffer paths

Audit Provability

The three audit findings that remained open after the enforcement pass asked for provable invariants, not just enforced ones. Each row below names the exact file the auditor greps, what they see, and the compile-time failure mode a bypass produces:

Audit finding Grep target What an auditor sees Bypass failure mode
F1: provable single access path AccountView.*data_ptr_unchecked|borrow_unchecked Every slice-returning accessor on hopper_native::AccountView is pub unsafe fn (borrow_unchecked, borrow_unchecked_mut) or explicitly low-level raw pointer (data_ptr_unchecked) consumed by same-crate internals and the documented raw-pointer escape hatches in hopper-runtime. Safe paths (try_borrow, try_borrow_mut, segment_ref, segment_mut) return Ref / RefMut with native borrow-state tracking Any call to borrow_unchecked* requires an unsafe block visible in the caller's source; obtaining a raw pointer is spelled _unchecked and dereferencing it remains unsafe
F2: compile-proven borrow safety HopperRefOnly Eight impls total (four sealed-trait impls, four marker-trait impls), all visible in crates/hopper-runtime/src/ref_only.rs. No macro expansion, no derive. The compile-fail fixture tests/compile_fail/ref_only_rejects_raw_ref.rs is the end-to-end proof Raw reference at the call site: error[E0277]: the trait bound '&mut u64: HopperRefOnly' is not satisfied
F3: entrypoint minimal sibling hopper-bench parity artifacts Hopper authorize = 431 CU, counter_access = 551 CU, deposit = 1669 CU, withdraw = 453 CU, binary 7.53 KiB; Anza Pinocchio authorize = 2512 CU, counter_access = 2539 CU, deposit = 3856 CU, withdraw = 2548 CU, binary 7.73 KiB; Quasar deposit = 1767 CU, withdraw = 603 CU, binary 6.27 KiB. Quasar's upstream vault does not implement authorize / counter_access, so those cells are n/a. Methodology is owned by the benchmark repo: pinned toolchain, equivalent-logic rule, shared vault contract, seed count, and exact command line Any regression is caught by the sibling parity runner, which records a cu_delta vs the Hopper baseline; the safety-correctness gate (unsigned_withdraw_rejected) excludes any framework that trades safety for speed

hopper-native Unsafe Surface (post-audit supplement, R10)

The original audit scope centred on hopper-core, hopper-runtime, and hopper-solana. The hopper-native crate (raw substrate, loader parsing, syscall wrappers) was reviewed during the audit closure pass but its unsafe surface was not enumerated in this document at the same level of rigour. This section closes that gap and lists every unsafe entry point in hopper-native with its invariants and test coverage pointer. Paired with the existing table above, this makes UNSAFE_INVARIANTS.md the complete ground-truth inventory for auditors.

hopper-native/src/account_view.rs

Entry point Kind Invariant Test coverage
AccountView::new_unchecked(ptr) pub unsafe fn ptr must point at a valid RuntimeAccount inside the loader-provided BPF input buffer and must remain dereferenceable for the lifetime of the returned view. Only constructed by raw_input::deserialize_accounts and lazy::parse_one_account, both of which consume a bounds-checked cursor tests/parse_harness.rs exercises the construction via deserialize_accounts end-to-end with deterministic fixtures
AccountView::owner(&self) pub unsafe fn Returns &Address into the BPF input buffer. Caller must not construct a mutable borrow of the same account's header concurrently. Hopper's safe helpers (read_owner, owned_by, check_owner) copy the bytes and drop the reference before returning account_view::tests::owner_readback (same-crate)
AccountView::assign(&self, new_owner) pub unsafe fn Account must be writable AND the program must own the account. Writes 32 bytes into the header owner slot close_flow_tests::owner_zeroed_on_close
AccountView::borrow_unchecked(&self) / borrow_unchecked_mut(&self) pub unsafe fn Caller must ensure no conflicting borrow via the safe API is live. Bypasses the 1-byte borrow_state field account_view::tests::unchecked_borrow_roundtrip, unsafe_boundary_tests.rs::unchecked_mut_conflicts (compile-fail check)
AccountView::segment_ref_unchecked(offset, size) / segment_mut_unchecked(offset, size) pub unsafe fn offset + size must be <= data_len. Caller owns alignment and aliasing for the returned slice. Safe variants (segment_ref, segment_mut) bounds-check and route through SegmentBorrowRegistry segment_bounds_tests.rs covers undersized, oversized, and overlapping borrows
AccountView::raw_ref::<T>() / raw_mut::<T>() pub unsafe fn T: Pod, size_of::<T>() <= data_len, no concurrent borrow. These are the Tier C hot-path accessors pod_tier_tests.rs::raw_ref_matches_safe_overlay (equivalence)
AccountView::resize_unchecked(new_len) pub unsafe fn new_len <= MAX_PERMITTED_DATA_INCREASE + original_len; caller has no live borrows into the data slice realloc_tests.rs::resize_growth_and_shrink
AccountView::close_unchecked(dest) pub unsafe fn Caller holds no live borrows into the closing account; writes CLOSE_SENTINEL into discriminator slot after transferring lamports close_flow_tests::close_sentinel_present

hopper-native/src/raw_input.rs

Entry point Kind Invariant Test coverage
deserialize_accounts::<MAX>() pub unsafe fn input must be the pointer the Solana loader passes to the BPF entrypoint. Walks marker bytes and canonical RuntimeAccount frames with strict alignment parse_instruction_frame_checked tests (lines 475-543 in raw_input.rs) mirror the same parser and cover malformed, forward-reference, self-reference, and EOF inputs
deserialize_accounts_fast::<MAX>() pub unsafe fn Same as above, plus the caller must be running on SVM ≥ 1.17 with the two-register entrypoint convention. Hopper's fast_entrypoint! macro is the only well-typed caller Same harness as eager variant
scan_instruction_frame() pub unsafe fn Input must be a valid BPF buffer. Returns the (program_id, data) pair without claiming any account slots; caller responsible for the subsequent scan parse_instruction_frame_checked tests
malformed_duplicate_marker(marker, slot) fn(..) -> ! Never returns. On-chain: calls sol_panic_. Off-chain: panics. Exists to close the pre-audit "Must-Fix #1" where an attacker-supplied forward duplicate reference fell through to account zero Covered by the forward-reference and self-reference fixtures in the checked-parser tests

hopper-native/src/lazy.rs

Entry point Kind Invariant Test coverage
lazy_deserialize(input) pub unsafe fn Same input contract as deserialize_accounts. Returns a LazyContext whose cursor points at the next unparsed account frame; no accounts are materialised yet lazy_tests.rs exercises single-account, multi-account, and duplicate-account partial scans
LazyContext::parse_one_account() unsafe fn (crate-private) Cursor must point at a valid marker or canonical frame boundary. Precondition checked by advance_cursor in the caller Covered transitively by lazy_tests.rs
LazyContext::advance_cursor() / advance_non_dup_cursor() unsafe fn (crate-private) Cursor must be inside the BPF buffer; advances by the account frame's declared size Same

hopper-native/src/pda.rs

Entry point Kind Invariant Test coverage
Inline unsafe in verify_program_address Bounded seed array construction from MaybeUninit All MaybeUninit slots are written before the slice is exposed to sol_sha256; assume_init_ref is called only after every slot is initialized pda_tests.rs::verify_program_address_sha256_only
Inline unsafe in based_try_find_program_address (3 blocks) Bump iteration with per-iteration seed mutation Each iteration writes a fresh bump byte into the last seed slot before the sha256 call. Safety comment at the top of the loop documents that the slot is always overwritten pda_tests.rs::bump_iteration_exhaustive
Inline unsafe in find_bump_for_address (3 blocks) Same pattern as based_try_find_program_address but skips sol_curve_validate_point. Safe because PDAs are off-curve by construction and the check compares the resulting address to a known-on-chain PDA pda_tests.rs::find_bump_skips_curve_validate

hopper-native/src/mem.rs

Entry point Kind Invariant Test coverage
memcpy(dst, src, n) pub unsafe fn dst and src must be valid for n bytes; regions must not overlap. Dispatches to sol_memcpy_ on-chain and core::ptr::copy_nonoverlapping off-chain mem_tests.rs::memcpy_roundtrip
memmove(dst, src, n) pub unsafe fn Regions may overlap mem_tests.rs::memmove_overlapping_slices
memset(dst, byte, n) pub unsafe fn dst valid for n bytes mem_tests.rs::memset_zero
memcmp(a, b, n, result) pub unsafe fn Both pointers valid for n bytes; result writable for one i32 mem_tests.rs::memcmp_equal_and_nonequal

hopper-native/src/cpi.rs (borrow-conflict invariant, expanded)

As of R7, cpi::invoke_unchecked and cpi::invoke_signed_unchecked now carry an explicit seven-item invariant list in their # Safety doc blocks. The inventory is:

  1. No aliasing borrows into any account in accounts for the call's duration.
  2. accounts corresponds to real accounts from the program entrypoint (address + signer/writable flags).
  3. Writability and signer flags match the instruction's declared requirements.
  4. Duplicate accounts impose caller responsibility for post-CPI re-borrow discipline.
  5. instruction.program_id / accounts / data pointers are valid for the call's lifetime.
  6. (signed variant) Every Signer seed derivation hashes to a signer address in accounts.
  7. (signed variant) Seed slice lifetimes exceed the call duration.

Checked variants (invoke, invoke_signed) enforce 2-4 and 6 before routing to the unchecked path; the typical caller should reach for those unless a CU measurement justifies bypassing validation.

Verification

cargo check --workspace --all-targets    # green (pre-existing deprecation warnings only)
cargo test  --workspace --no-fail-fast   # 740 passed, 0 failed, 133 ignored
cargo test --test ui --features proc-macros --locked  # 4 trybuild tests, 17 compile-fail fixtures
cargo test -p hopper-trybuild --locked                # 2 trybuild tests, 6 fixtures, all pass
cargo test  --test require_macros        # 16 guard-macro tests pass
cargo test  --test migrate_integration --features proc-macros  # 8 migration-chain tests pass
cargo test  --test hybrid_tail_integration --features proc-macros  # 8 dynamic-tail tests pass
cargo test  -p hopper-schema rust_client # 9 Rust-client-gen tests pass
cd fuzz && cargo check                    # fuzz crate structure compiles
cargo build-sbf --manifest-path examples/hopper-parity-vault/Cargo.toml       # 15 KiB .so
cargo build-sbf --manifest-path examples/hopper-token-2022-vault/Cargo.toml   # 20 KiB .so
cargo run -p hopper-cli -- verify @examples/sample-manifest.json              # manifest integrity + binary scan
cargo run -p hopper-cli -- compile --emit rust-client @examples/sample-manifest.json  # Rust client emits