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/// Zeroes the account data and writes the close sentinel to prevent revival.
25#[inline]
26pub fn safe_close(account: &AccountView, destination: &AccountView) -> ProgramResult {
27    let lamports = account.lamports();
28    if lamports == 0 {
29        return Ok(());
30    }
31
32    // Add to destination
33    let new_dest = destination
34        .lamports()
35        .checked_add(lamports)
36        .ok_or(ProgramError::ArithmeticOverflow)?;
37    destination.set_lamports(new_dest);
38
39    // Drain source
40    account.set_lamports(0);
41
42    // Zero account data
43    let mut data = account.try_borrow_mut()?;
44    zero_init(&mut data);
45
46    Ok(())
47}
48
49/// Close with sentinel -- writes `CLOSE_SENTINEL` to byte 0 after zeroing.
50#[inline]
51pub fn safe_close_with_sentinel(account: &AccountView, destination: &AccountView) -> ProgramResult {
52    safe_close(account, destination)?;
53
54    // Write sentinel to prevent revival
55    let mut data = account.try_borrow_mut()?;
56    if !data.is_empty() {
57        data[0] = CLOSE_SENTINEL;
58    }
59
60    Ok(())
61}
62
63/// Reallocate an account to a new size.
64///
65/// Handles the rent-exemption delta and transfers lamports from/to `payer`.
66#[inline]
67pub fn safe_realloc(account: &AccountView, new_size: usize, payer: &AccountView) -> ProgramResult {
68    account.resize(new_size)?;
69
70    // Compute new rent and transfer delta
71    let rent_needed = rent_exempt_min_internal(new_size);
72    let current_lamports = account.lamports();
73
74    if rent_needed > current_lamports {
75        let deficit = rent_needed - current_lamports;
76        // Transfer from payer to account
77        let payer_lamports = payer
78            .lamports()
79            .checked_sub(deficit)
80            .ok_or(ProgramError::InsufficientFunds)?;
81        payer.set_lamports(payer_lamports);
82        let acct_lamports = account
83            .lamports()
84            .checked_add(deficit)
85            .ok_or(ProgramError::ArithmeticOverflow)?;
86        account.set_lamports(acct_lamports);
87    }
88
89    Ok(())
90}
91
92// Internal rent calculation (matches Solana's formula).
93pub(crate) fn rent_exempt_min_internal(data_len: usize) -> u64 {
94    // Solana formula: (128 + data_len) * 6960 lamports (approximately)
95    // This is the standard exemption calculation.
96    ((128 + data_len) as u64) * 6960
97}