Skip to main content

hopper_core/frame/
mod.rs

1//! Borrowed-state execution context.
2//!
3//! The `Frame` is Hopper's execution model. It wraps the instruction's accounts
4//! and data, enforcing single-mutable-borrow discipline and phased execution.
5//!
6//! ## Execution Phases
7//!
8//! 1. **Resolve** -- Parse accounts from the input slice into named typed slots
9//! 2. **Validate** -- Run the validation graph (account-local, cross-account, state-transition)
10//! 3. **Borrow** -- Obtain zero-copy overlays with borrow discipline
11//! 4. **Mutate** -- Execute state changes through verified mutable references
12//! 5. **Emit** -- Fire events
13//! 6. **Commit** -- (implicit: Solana runtime commits on success)
14//!
15//! The `Frame` ensures that:
16//! - Each account is borrowed at most once mutably
17//! - Immutable borrows can coexist
18//! - Validation runs before mutation
19//! - Events are emitted after state changes
20
21pub mod args;
22pub mod phase;
23
24use crate::account::SliceCursor;
25use crate::account::{FixedLayout, Pod, HEADER_LEN};
26use hopper_runtime::segment_borrow::SegmentBorrowRegistry;
27use hopper_runtime::{
28    error::ProgramError, AccountView, Address, ProgramResult, Ref, RefMut, SegRef, SegRefMut,
29    SegmentLease,
30};
31
32/// Maximum accounts in a single frame. Matches Solana's transaction limit.
33pub const MAX_FRAME_ACCOUNTS: usize = 64;
34
35/// Execution frame holding the instruction's accounts and data.
36///
37/// `Frame` is the entry point for Hopper's phased execution model.
38/// It tracks which accounts have been borrowed (mutably or immutably)
39/// to prevent aliasing violations at runtime.
40pub struct Frame<'a> {
41    /// Program ID that is executing.
42    program_id: &'a Address,
43    /// Raw account views.
44    accounts: &'a [AccountView],
45    /// Instruction data cursor.
46    ix_data: SliceCursor<'a>,
47    /// Borrow tracking: bit N = 1 means account N is mutably borrowed.
48    /// This is a runtime check -- not as strong as the borrow checker, but
49    /// catches the most dangerous pattern (double-mutable-borrow).
50    mutable_borrows: u64,
51    /// Segment-level borrow tracking for fine-grained conflict detection.
52    /// Allows concurrent mutable access to non-overlapping regions of the
53    /// same account, the key safety property missing from raw pointer access.
54    segment_borrows: SegmentBorrowRegistry,
55}
56
57impl<'a> Frame<'a> {
58    /// Create a new execution frame.
59    #[inline(always)]
60    pub fn new(
61        program_id: &'a Address,
62        accounts: &'a [AccountView],
63        instruction_data: &'a [u8],
64    ) -> Result<Self, ProgramError> {
65        if accounts.len() > MAX_FRAME_ACCOUNTS {
66            return Err(ProgramError::InvalidArgument);
67        }
68        Ok(Self {
69            program_id,
70            accounts,
71            ix_data: SliceCursor::new(instruction_data),
72            mutable_borrows: 0,
73            segment_borrows: SegmentBorrowRegistry::new(),
74        })
75    }
76
77    /// Program ID.
78    #[inline(always)]
79    pub fn program_id(&self) -> &Address {
80        self.program_id
81    }
82
83    /// Number of accounts in this frame.
84    #[inline(always)]
85    pub fn account_count(&self) -> usize {
86        self.accounts.len()
87    }
88
89    /// Get raw account view by index.
90    #[inline(always)]
91    pub fn account_view(&self, index: usize) -> Result<&AccountView, ProgramError> {
92        self.accounts
93            .get(index)
94            .ok_or(ProgramError::NotEnoughAccountKeys)
95    }
96
97    /// Get instruction data cursor.
98    #[inline(always)]
99    pub fn ix_data(&mut self) -> &mut SliceCursor<'a> {
100        &mut self.ix_data
101    }
102
103    /// Get raw instruction data.
104    #[inline(always)]
105    pub fn ix_data_raw(&self) -> &[u8] {
106        self.ix_data.data_from_position()
107    }
108
109    // --- Immutable Account Access -----------------------------------
110
111    /// Get an immutable account view (no borrow tracking needed for reads).
112    #[inline(always)]
113    pub fn account(&self, index: usize) -> Result<FrameAccount<'_>, ProgramError> {
114        let view = self
115            .accounts
116            .get(index)
117            .ok_or(ProgramError::NotEnoughAccountKeys)?;
118        Ok(FrameAccount { view })
119    }
120
121    // --- Mutable Account Access (with borrow tracking) -------------
122
123    /// Get a mutable account view with runtime borrow checking.
124    ///
125    /// Returns an error if this account is already borrowed mutably.
126    /// This prevents the most dangerous aliasing pattern in Solana programs.
127    #[inline]
128    pub fn account_mut(&mut self, index: usize) -> Result<FrameAccountMut<'_>, ProgramError> {
129        if index >= self.accounts.len() {
130            return Err(ProgramError::NotEnoughAccountKeys);
131        }
132
133        let bit = 1u64 << (index as u32);
134        if self.mutable_borrows & bit != 0 {
135            // Already mutably borrowed -- prevent aliasing.
136            return Err(ProgramError::AccountBorrowFailed);
137        }
138
139        self.mutable_borrows |= bit;
140        let view = &self.accounts[index];
141
142        Ok(FrameAccountMut {
143            view,
144            borrow_mask: &mut self.mutable_borrows,
145            bit,
146        })
147    }
148
149    // --- Segment-Level Access (fine-grained borrow tracking) --------
150
151    /// Get the segment borrow registry for direct manipulation.
152    #[inline(always)]
153    pub fn segment_borrows(&self) -> &SegmentBorrowRegistry {
154        &self.segment_borrows
155    }
156
157    /// Get the mutable segment borrow registry.
158    #[inline(always)]
159    pub fn segment_borrows_mut(&mut self) -> &mut SegmentBorrowRegistry {
160        &mut self.segment_borrows
161    }
162
163    /// Read a typed value from a segment of an account's data region.
164    ///
165    /// Registers a **read** borrow for the given byte range, then projects
166    /// the pointer through the live byte-borrow guard into a `Ref<'_, T>`.
167    /// Returns an error if the range conflicts with an existing write
168    /// borrow on the same account.
169    ///
170    /// `offset` is relative to the layout body (after the 16-byte header).
171    ///
172    /// # Preferred path
173    ///
174    /// Most programs don't need to construct a `Frame` at all, the
175    /// `hopper_runtime::Context` handler signature gives you
176    /// `ctx.segment_ref::<T>(index, abs_offset)` with the same tightened
177    /// Pod contract, the same RAII guard, and none of the phased
178    /// execution bookkeeping. Reach for `Frame::segment_ref` only when
179    /// you're inside the advanced `frame`-gated execution model.
180    ///
181    /// # Safety Contract
182    ///
183    /// - T must be `Pod + FixedLayout` (safe to interpret from any bit pattern,
184    ///   alignment-1, no padding).
185    /// - Bounds are checked at runtime.
186    /// - Borrow conflicts are checked at runtime.
187    /// - The returned [`SegRef<T>`] owns both the byte-slice borrow and
188    ///   a RAII lease on the segment registry entry. Dropping it
189    ///   releases **both**, no sticky-ledger residue from post-audit.
190    #[inline]
191    pub fn segment_ref<'f, T: Pod + FixedLayout>(
192        &'f mut self,
193        index: usize,
194        offset: u32,
195    ) -> Result<SegRef<'f, T>, ProgramError> {
196        let view = self
197            .accounts
198            .get(index)
199            .ok_or(ProgramError::NotEnoughAccountKeys)?;
200        let data = view.try_borrow()?;
201
202        let abs_offset = (HEADER_LEN as u32)
203            .checked_add(offset)
204            .ok_or(ProgramError::ArithmeticOverflow)?;
205        let end = abs_offset
206            .checked_add(T::SIZE as u32)
207            .ok_or(ProgramError::ArithmeticOverflow)?;
208        if end as usize > data.len() {
209            return Err(ProgramError::AccountDataTooSmall);
210        }
211
212        let borrow = self.segment_borrows.register_leased_read(
213            view.address(),
214            abs_offset,
215            T::SIZE as u32,
216        )?;
217
218        // SAFETY: T is Pod + FixedLayout (all bit patterns valid, align-1).
219        // Bounds checked above. `Ref::project` consumes the byte-slice
220        // guard and yields a `Ref<T>` whose lifetime is tied to the
221        // account borrow; the `SegmentLease` we build immediately after
222        // releases the registry entry on drop.
223        // 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.
224        let ptr = unsafe { data.as_bytes_ptr().add(abs_offset as usize) as *const T };
225        let inner: Ref<'f, T> = unsafe { data.project(ptr) };
226        // SAFETY: `borrow` was just registered in `self.segment_borrows`;
227        // the lease is the sole releaser for that entry.
228        let lease: SegmentLease<'f> =
229            unsafe { SegmentLease::new(&mut self.segment_borrows, borrow) };
230        Ok(SegRef::new(inner, lease))
231    }
232
233    /// Get a mutable typed reference to a segment of an account's data.
234    ///
235    /// Registers a **write** borrow for the given byte range, then projects
236    /// the pointer through the live byte-borrow guard into a `RefMut<'_, T>`.
237    /// Returns an error if the range overlaps any existing borrow (read or
238    /// write) on the same account.
239    ///
240    /// This is the core primitive that makes Hopper strictly better than
241    /// raw Pinocchio: you get the same pointer arithmetic, but with
242    /// segment-level conflict detection that prevents aliasing bugs.
243    ///
244    /// `offset` is relative to the layout body (after the 16-byte header).
245    ///
246    /// # Safety Contract
247    ///
248    /// - T must be `Pod + FixedLayout`.
249    /// - Bounds are checked at runtime.
250    /// - Borrow conflicts are checked at runtime.
251    /// - The returned [`SegRefMut<T>`] carries both the account-level
252    ///   exclusive byte guard and a RAII registry lease, dropping it
253    ///   releases the full borrow cleanly, so sequential patterns on
254    ///   the same segment compose like ordinary Rust borrows.
255    ///
256    /// # Example
257    ///
258    /// ```ignore
259    /// // Only borrows the "balance" region [32..40), not the entire account.
260    /// {
261    ///     let mut balance = frame.segment_mut::<WireU64>(0, 32)?;
262    ///     balance.set(balance.get() + amount);
263    /// } // SegRefMut drops here, releasing both guards.
264    ///
265    /// // Now we can re-borrow the same (or a different) segment safely.
266    /// let mut balance_again = frame.segment_mut::<WireU64>(0, 32)?;
267    /// ```
268    #[inline]
269    pub fn segment_mut<'f, T: Pod + FixedLayout>(
270        &'f mut self,
271        index: usize,
272        offset: u32,
273    ) -> Result<SegRefMut<'f, T>, ProgramError> {
274        let view = self
275            .accounts
276            .get(index)
277            .ok_or(ProgramError::NotEnoughAccountKeys)?;
278
279        // Check writable before doing anything else.
280        if !view.is_writable() {
281            return Err(ProgramError::InvalidAccountData);
282        }
283
284        let data = view.try_borrow_mut()?;
285        let abs_offset = (HEADER_LEN as u32)
286            .checked_add(offset)
287            .ok_or(ProgramError::ArithmeticOverflow)?;
288        let end = abs_offset
289            .checked_add(T::SIZE as u32)
290            .ok_or(ProgramError::ArithmeticOverflow)?;
291        if end as usize > data.len() {
292            return Err(ProgramError::AccountDataTooSmall);
293        }
294
295        let borrow = self.segment_borrows.register_leased_write(
296            view.address(),
297            abs_offset,
298            T::SIZE as u32,
299        )?;
300
301        // SAFETY: as above; the projected `RefMut<T>` inherits the
302        // byte-slice exclusive borrow, and the lease ensures the
303        // registry entry is swap-removed on drop.
304        let bytes_ptr = (&*data) as *const [u8] as *mut [u8] as *mut u8;
305        // 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.
306        let ptr = unsafe { bytes_ptr.add(abs_offset as usize) as *mut T };
307        let inner: RefMut<'f, T> = unsafe { data.project(ptr) };
308        let lease: SegmentLease<'f> =
309            // 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.
310            unsafe { SegmentLease::new(&mut self.segment_borrows, borrow) };
311        Ok(SegRefMut::new(inner, lease))
312    }
313
314    /// Unsafe escape hatch for performance-critical paths.
315    ///
316    /// Skips borrow tracking entirely. The caller takes full responsibility
317    /// for aliasing safety. Returns a `RefMut<T>` so the borrow guard is
318    /// still tied to the returned value's lifetime, the "unchecked" part
319    /// is only the conflict-detection skip, not the lifetime tying.
320    ///
321    /// # Safety
322    ///
323    /// The caller must guarantee no other mutable reference to the same
324    /// byte range exists for the duration of the returned reference, and
325    /// that no overlapping segment borrow has been registered.
326    #[inline(always)]
327    pub unsafe fn segment_mut_unchecked<T: Pod + FixedLayout>(
328        &self,
329        index: usize,
330        offset: u32,
331    ) -> Result<RefMut<'_, T>, ProgramError> {
332        let view = self
333            .accounts
334            .get(index)
335            .ok_or(ProgramError::NotEnoughAccountKeys)?;
336        let data = view.try_borrow_mut()?;
337
338        let abs_offset = (HEADER_LEN as u32)
339            .checked_add(offset)
340            .ok_or(ProgramError::ArithmeticOverflow)?;
341        let end = abs_offset
342            .checked_add(T::SIZE as u32)
343            .ok_or(ProgramError::ArithmeticOverflow)?;
344        if end as usize > data.len() {
345            return Err(ProgramError::AccountDataTooSmall);
346        }
347
348        let bytes_ptr = (&*data) as *const [u8] as *mut [u8] as *mut u8;
349        // 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.
350        let ptr = unsafe { bytes_ptr.add(abs_offset as usize) as *mut T };
351        Ok(unsafe { data.project(ptr) })
352    }
353
354    // --- Validation Helpers -----------------------------------------
355
356    /// Validate that account at `index` is a signer.
357    #[inline(always)]
358    pub fn require_signer(&self, index: usize) -> ProgramResult {
359        crate::check::check_signer(self.account_view(index)?)
360    }
361
362    /// Validate that account at `index` is writable.
363    #[inline(always)]
364    pub fn require_writable(&self, index: usize) -> ProgramResult {
365        crate::check::check_writable(self.account_view(index)?)
366    }
367
368    /// Validate that account at `index` is owned by this program.
369    #[inline(always)]
370    pub fn require_owned(&self, index: usize) -> ProgramResult {
371        crate::check::check_owner(self.account_view(index)?, self.program_id)
372    }
373
374    /// Validate signer + writable (common pattern for authority accounts).
375    #[inline(always)]
376    pub fn require_authority(&self, index: usize) -> ProgramResult {
377        let view = self.account_view(index)?;
378        crate::check::check_signer(view)?;
379        crate::check::check_writable(view)?;
380        Ok(())
381    }
382
383    /// Validate two accounts are unique.
384    #[inline(always)]
385    pub fn require_unique(&self, a: usize, b: usize) -> ProgramResult {
386        let va = self.account_view(a)?;
387        let vb = self.account_view(b)?;
388        crate::check::check_accounts_unique(va, vb)
389    }
390
391    /// Require an account matches a specific program address.
392    #[inline(always)]
393    pub fn require_program(&self, index: usize, program: &Address) -> ProgramResult {
394        crate::check::check_address(self.account_view(index)?, program)
395    }
396}
397
398/// Immutable account view within a Frame.
399pub struct FrameAccount<'a> {
400    view: &'a AccountView,
401}
402
403impl<'a> FrameAccount<'a> {
404    /// The underlying AccountView.
405    #[inline(always)]
406    pub fn view(&self) -> &AccountView {
407        self.view
408    }
409
410    /// The account's address.
411    #[inline(always)]
412    pub fn address(&self) -> &Address {
413        self.view.address()
414    }
415
416    /// Borrow account data (read-only).
417    #[inline(always)]
418    pub fn data(&self) -> Result<Ref<'a, [u8]>, ProgramError> {
419        self.view.try_borrow()
420    }
421
422    /// Lamports balance.
423    #[inline(always)]
424    pub fn lamports(&self) -> u64 {
425        self.view.lamports()
426    }
427
428    /// Is this account a signer?
429    #[inline(always)]
430    pub fn is_signer(&self) -> bool {
431        self.view.is_signer()
432    }
433
434    /// Is this account writable?
435    #[inline(always)]
436    pub fn is_writable(&self) -> bool {
437        self.view.is_writable()
438    }
439}
440
441/// Mutable account view within a Frame.
442///
443/// When this is dropped, the mutable borrow tracking bit is cleared,
444/// allowing the account to be re-borrowed.
445pub struct FrameAccountMut<'a> {
446    view: &'a AccountView,
447    borrow_mask: &'a mut u64,
448    bit: u64,
449}
450
451impl<'a> FrameAccountMut<'a> {
452    /// The underlying AccountView.
453    #[inline(always)]
454    pub fn view(&self) -> &AccountView {
455        self.view
456    }
457
458    /// The account's address.
459    #[inline(always)]
460    pub fn address(&self) -> &Address {
461        self.view.address()
462    }
463
464    /// Borrow account data (read-only).
465    #[inline(always)]
466    pub fn data(&self) -> Result<Ref<'a, [u8]>, ProgramError> {
467        self.view.try_borrow()
468    }
469
470    /// Borrow account data (mutable).
471    #[inline(always)]
472    pub fn data_mut(&self) -> Result<RefMut<'a, [u8]>, ProgramError> {
473        self.view.try_borrow_mut()
474    }
475
476    /// Lamports balance.
477    #[inline(always)]
478    pub fn lamports(&self) -> u64 {
479        self.view.lamports()
480    }
481}
482
483impl<'a> Drop for FrameAccountMut<'a> {
484    fn drop(&mut self) {
485        // Release the borrow tracking bit.
486        *self.borrow_mask &= !self.bit;
487    }
488}
489
490// ══════════════════════════════════════════════════════════════════════
491//  Audit regression tests
492// ══════════════════════════════════════════════════════════════════════
493//
494// Lock in the Hopper Safety Audit's top-priority fix: Frame's segment
495// accessors now hand back `Ref<T>` / `RefMut<T>` that keep the
496// underlying account borrow alive for their full lifetime. The
497// pre-audit version dropped the byte-slice guard before returning the
498// typed reference, which is silent UB. These tests prove the guard is
499// still live at use time.
500#[cfg(all(test, feature = "hopper-native-backend"))]
501mod audit_tests {
502    use super::*;
503    use hopper_native::{
504        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
505    };
506
507    #[repr(C)]
508    #[derive(Clone, Copy)]
509    struct Counter {
510        value: u64,
511    }
512
513    unsafe impl hopper_runtime::__hopper_native::bytemuck::Zeroable for Counter {}
514    unsafe impl hopper_runtime::__hopper_native::bytemuck::Pod for Counter {}
515    unsafe impl hopper_runtime::Pod for Counter {}
516
517    impl crate::account::FixedLayout for Counter {
518        const SIZE: usize = 8;
519    }
520
521    fn make_account(data_len: usize, seed: u8) -> (std::vec::Vec<u8>, AccountView) {
522        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + data_len];
523        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
524        // 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.
525        unsafe {
526            raw.write(RuntimeAccount {
527                borrow_state: NOT_BORROWED,
528                is_signer: 1,
529                is_writable: 1,
530                executable: 0,
531                resize_delta: 0,
532                address: NativeAddress::new_from_array([seed; 32]),
533                owner: NativeAddress::new_from_array([2; 32]),
534                lamports: 42,
535                data_len: data_len as u64,
536            });
537        }
538        // Zero the Hopper header region so the frame doesn't trip on
539        // uninitialized bytes later.
540        // 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.
541        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
542        let view = unsafe { core::mem::transmute::<NativeAccountView, AccountView>(backend) };
543        (backing, view)
544    }
545
546    fn new_frame<'a>(program_id: &'a Address, accounts: &'a [AccountView]) -> Frame<'a> {
547        Frame::new(program_id, accounts, &[]).unwrap()
548    }
549
550    #[test]
551    fn frame_segment_mut_writes_through_ref_mut() {
552        // This test is the ground-truth for the audit fix: the fact
553        // that we can write through `RefMut<Counter>` returned by
554        // `Frame::segment_mut` and see the write persist proves the
555        // projection and guard release are now correctly tied together.
556        // Pre-audit this same code compiled but the byte-slice guard
557        // had already been dropped when `segment_mut` returned, any
558        // overlapping borrow tracking was racing against stale state.
559        let (_backing, account) = make_account(HEADER_LEN + 8, 1);
560        let program_id = NativeAddress::new_from_array([9; 32]);
561        let hopper_program_id =
562            // 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.
563            unsafe { core::mem::transmute::<NativeAddress, Address>(program_id) };
564        let accounts = [account];
565        let mut frame = new_frame(&hopper_program_id, &accounts);
566
567        {
568            let mut counter: SegRefMut<'_, Counter> = frame.segment_mut::<Counter>(0, 0).unwrap();
569            counter.value = 7;
570            // counter (SegRefMut with byte-slice guard AND registry
571            // lease) drops here, releasing both.
572        }
573
574        // Reopen the account through the account-view path; the
575        // segment registry already recorded the write for the whole
576        // instruction, so we confirm persistence by rereading the raw
577        // bytes via the underlying account view.
578        let bytes = frame.account(0).unwrap().data().unwrap();
579        let slice: &[u8] = &*bytes;
580        // 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.
581        let raw_u64 =
582            unsafe { core::ptr::read_unaligned(slice.as_ptr().add(HEADER_LEN) as *const u64) };
583        assert_eq!(raw_u64, 7);
584    }
585
586    #[test]
587    fn frame_segment_ref_returns_live_guard() {
588        // Seed the counter via direct byte access, then verify a
589        // `segment_ref` returned guard lets us read that value. The
590        // crucial property this exercises: `Ref<'_, Counter>` deref
591        // into `Counter` after `segment_ref` returns, which pre-audit
592        // would have been reading through a dropped byte-slice guard.
593        let (_backing, account) = make_account(HEADER_LEN + 8, 2);
594        {
595            let mut bytes = account.try_borrow_mut().unwrap();
596            // 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.
597            let slot = unsafe { bytes.as_bytes_mut_ptr().add(HEADER_LEN) as *mut u64 };
598            // 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.
599            unsafe { core::ptr::write_unaligned(slot, 99) };
600        }
601        let program_id = NativeAddress::new_from_array([9; 32]);
602        let hopper_program_id =
603            // 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.
604            unsafe { core::mem::transmute::<NativeAddress, Address>(program_id) };
605        let accounts = [account];
606        let mut frame = new_frame(&hopper_program_id, &accounts);
607
608        let reader: SegRef<'_, Counter> = frame.segment_ref::<Counter>(0, 0).unwrap();
609        assert_eq!(reader.value, 99);
610    }
611
612    /// Audit regression: post-fix, dropping a `SegRefMut` from
613    /// `Frame::segment_mut` must release the segment-registry lease so
614    /// a sequential re-acquire on the same region succeeds. Pre-audit
615    /// the sticky ledger blocked this for the rest of the instruction.
616    #[test]
617    fn frame_segment_lease_releases_on_drop() {
618        let (_backing, account) = make_account(HEADER_LEN + 8, 3);
619        let program_id = NativeAddress::new_from_array([9; 32]);
620        let hopper_program_id =
621            // 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.
622            unsafe { core::mem::transmute::<NativeAddress, Address>(program_id) };
623        let accounts = [account];
624        let mut frame = new_frame(&hopper_program_id, &accounts);
625
626        // First write.
627        {
628            let mut w: SegRefMut<'_, Counter> = frame.segment_mut::<Counter>(0, 0).unwrap();
629            w.value = 50;
630        }
631        assert_eq!(frame.segment_borrows().len(), 0);
632
633        // Second write on the same region, pre-audit this returned
634        // `AccountBorrowFailed`; now it succeeds because the prior
635        // lease has been released.
636        {
637            let mut w: SegRefMut<'_, Counter> = frame.segment_mut::<Counter>(0, 0).unwrap();
638            assert_eq!(w.value, 50);
639            w.value = 77;
640        }
641        assert_eq!(frame.segment_borrows().len(), 0);
642
643        let r: SegRef<'_, Counter> = frame.segment_ref::<Counter>(0, 0).unwrap();
644        assert_eq!(r.value, 77);
645    }
646}