Skip to main content

hopper_core/account/
lifecycle.rs

1//! Account lifecycle operations: init, close, realloc.
2
3use hopper_runtime::{error::ProgramError, AccountView, ProgramResult};
4
5/// Sentinel byte written to byte 0 when an account is closed.
6/// Prevents account revival attacks.
7pub const CLOSE_SENTINEL: u8 = 0xFF;
8
9/// Zero-initialize a byte slice. Must be called before `write_header`.
10///
11/// Solana does NOT guarantee zeroed account data on creation.
12/// Always call this on freshly allocated accounts.
13#[inline(always)]
14pub fn zero_init(data: &mut [u8]) {
15    // NOTE: Using a byte-by-byte fill to avoid any alignment issues.
16    // The compiler will optimize this to memset.
17    for byte in data.iter_mut() {
18        *byte = 0;
19    }
20}
21
22/// Safely close an account by draining all lamports to `destination`.
23///
24/// This is the advanced no-sentinel primitive: it zeroes account data but does
25/// not mark byte 0 with [`CLOSE_SENTINEL`]. Generated Hopper close helpers use
26/// [`safe_close_with_sentinel`] via `hopper_close!`.
27#[inline]
28pub fn safe_close(account: &AccountView, destination: &AccountView) -> ProgramResult {
29    let lamports = account.lamports();
30    if lamports == 0 {
31        return Ok(());
32    }
33
34    // Add to destination
35    let new_dest = destination
36        .lamports()
37        .checked_add(lamports)
38        .ok_or(ProgramError::ArithmeticOverflow)?;
39    destination.set_lamports(new_dest);
40
41    // Drain source
42    account.set_lamports(0);
43
44    // Zero account data
45    let mut data = account.try_borrow_mut()?;
46    zero_init(&mut data);
47
48    Ok(())
49}
50
51/// Close with sentinel -- writes `CLOSE_SENTINEL` to byte 0 after zeroing.
52#[inline]
53pub fn safe_close_with_sentinel(account: &AccountView, destination: &AccountView) -> ProgramResult {
54    safe_close(account, destination)?;
55
56    // Write sentinel to prevent revival
57    let mut data = account.try_borrow_mut()?;
58    if !data.is_empty() {
59        data[0] = CLOSE_SENTINEL;
60    }
61
62    Ok(())
63}
64
65/// Reallocate an account to a new size.
66///
67/// Handles the rent-exemption delta and transfers lamports from `payer` after
68/// preflighting rent and balance checks. The order is intentional: the function
69/// does all arithmetic and funding validation before the account data length is
70/// changed, then performs the resize, then applies the lamport movement.
71#[inline]
72pub fn safe_realloc(account: &AccountView, new_size: usize, payer: &AccountView) -> ProgramResult {
73    let rent_needed = rent_exempt_min_internal(new_size)?;
74    let current_lamports = account.lamports();
75    let deficit = rent_needed.saturating_sub(current_lamports);
76
77    let payer_lamports_after = if deficit > 0 {
78        Some(
79            payer
80                .lamports()
81                .checked_sub(deficit)
82                .ok_or(ProgramError::InsufficientFunds)?,
83        )
84    } else {
85        None
86    };
87    let account_lamports_after = if deficit > 0 {
88        Some(
89            current_lamports
90                .checked_add(deficit)
91                .ok_or(ProgramError::ArithmeticOverflow)?,
92        )
93    } else {
94        None
95    };
96
97    account.resize(new_size)?;
98
99    if let (Some(payer_lamports), Some(account_lamports)) =
100        (payer_lamports_after, account_lamports_after)
101    {
102        payer.set_lamports(payer_lamports);
103        account.set_lamports(account_lamports);
104    }
105
106    Ok(())
107}
108
109// Internal rent calculation (matches Solana's formula).
110pub(crate) fn rent_exempt_min_internal(data_len: usize) -> Result<u64, ProgramError> {
111    // Solana formula: (128 + data_len) * 6960 lamports (approximately)
112    // This is the standard exemption calculation.
113    let data_len = u64::try_from(data_len).map_err(|_| ProgramError::ArithmeticOverflow)?;
114    data_len
115        .checked_add(128)
116        .and_then(|bytes| bytes.checked_mul(6960))
117        .ok_or(ProgramError::ArithmeticOverflow)
118}
119
120#[cfg(all(test, feature = "hopper-native-backend"))]
121mod tests {
122    use super::*;
123    use hopper_native::{
124        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
125    };
126
127    fn make_account(data_len: usize, lamports: u64, seed: u8) -> (std::vec::Vec<u8>, AccountView) {
128        let mut backing = std::vec![0u8; RuntimeAccount::SIZE + data_len];
129        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
130        // SAFETY: The test owns `backing`, writes one valid RuntimeAccount header,
131        // and keeps the backing buffer alive for the returned AccountView.
132        unsafe {
133            raw.write(RuntimeAccount {
134                borrow_state: NOT_BORROWED,
135                is_signer: 1,
136                is_writable: 1,
137                executable: 0,
138                resize_delta: 0,
139                address: NativeAddress::new_from_array([seed; 32]),
140                owner: NativeAddress::new_from_array([2; 32]),
141                lamports,
142                data_len: data_len as u64,
143            });
144        }
145        // SAFETY: `raw` points at the RuntimeAccount header just initialized above.
146        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
147        // SAFETY: hopper-runtime AccountView is repr(transparent) over the active
148        // hopper-native AccountView when the hopper-native backend feature is enabled.
149        let view = unsafe { core::mem::transmute::<NativeAccountView, AccountView>(backend) };
150        (backing, view)
151    }
152
153    #[test]
154    fn safe_realloc_checks_funding_before_resize() {
155        let (_account_backing, account) = make_account(16, 0, 1);
156        let (_payer_backing, payer) = make_account(0, 1, 2);
157
158        let result = safe_realloc(&account, 64, &payer);
159
160        assert_eq!(result, Err(ProgramError::InsufficientFunds));
161        assert_eq!(account.data_len(), 16);
162        assert_eq!(account.lamports(), 0);
163        assert_eq!(payer.lamports(), 1);
164    }
165
166    #[test]
167    fn safe_realloc_moves_lamports_after_successful_resize() {
168        let needed = rent_exempt_min_internal(32).unwrap();
169        let (_account_backing, account) = make_account(16, 0, 3);
170        let (_payer_backing, payer) = make_account(0, needed, 4);
171
172        safe_realloc(&account, 32, &payer).unwrap();
173
174        assert_eq!(account.data_len(), 32);
175        assert_eq!(account.lamports(), needed);
176        assert_eq!(payer.lamports(), 0);
177    }
178}