spl_governance_tools/
account.rs

1//! General purpose account utility functions
2
3use {
4    crate::error::GovernanceToolsError,
5    borsh::{BorshDeserialize, BorshSerialize},
6    solana_program::{
7        account_info::AccountInfo,
8        borsh0_10::try_from_slice_unchecked,
9        msg,
10        program::{invoke, invoke_signed},
11        program_error::ProgramError,
12        program_pack::IsInitialized,
13        pubkey::Pubkey,
14        rent::Rent,
15        system_instruction::{self, create_account},
16        system_program,
17        sysvar::Sysvar,
18    },
19};
20
21/// Trait for accounts to return their max size
22pub trait AccountMaxSize {
23    /// Returns max account size or None if max size is not known and actual
24    /// instance size should be used
25    fn get_max_size(&self) -> Option<usize> {
26        None
27    }
28}
29
30/// Creates a new account and serializes data into it using AccountMaxSize to
31/// determine the account's size
32pub fn create_and_serialize_account<'a, T: BorshSerialize + AccountMaxSize>(
33    payer_info: &AccountInfo<'a>,
34    account_info: &AccountInfo<'a>,
35    account_data: &T,
36    program_id: &Pubkey,
37    system_info: &AccountInfo<'a>,
38) -> Result<(), ProgramError> {
39    // Assert the account is not initialized yet
40    if !(account_info.data_is_empty() && *account_info.owner == system_program::id()) {
41        return Err(GovernanceToolsError::AccountAlreadyInitialized.into());
42    }
43
44    let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() {
45        (None, max_size)
46    } else {
47        let serialized_data = account_data.try_to_vec()?;
48        let account_size = serialized_data.len();
49        (Some(serialized_data), account_size)
50    };
51
52    let rent = Rent::get()?;
53
54    let create_account_instruction = create_account(
55        payer_info.key,
56        account_info.key,
57        rent.minimum_balance(account_size),
58        account_size as u64,
59        program_id,
60    );
61
62    invoke(
63        &create_account_instruction,
64        &[
65            payer_info.clone(),
66            account_info.clone(),
67            system_info.clone(),
68        ],
69    )?;
70
71    if let Some(serialized_data) = serialized_data {
72        account_info
73            .data
74            .borrow_mut()
75            .copy_from_slice(&serialized_data);
76    } else {
77        borsh::to_writer(&mut account_info.data.borrow_mut()[..], account_data)?;
78    }
79
80    Ok(())
81}
82
83/// Creates a new account and serializes data into it using the provided seeds
84/// to invoke signed CPI call The owner of the account is set to the PDA program
85/// Note: This functions also checks the provided account PDA matches the
86/// supplied seeds
87#[allow(clippy::too_many_arguments)]
88pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSize>(
89    payer_info: &AccountInfo<'a>,
90    account_info: &AccountInfo<'a>,
91    account_data: &T,
92    account_address_seeds: &[&[u8]],
93    program_id: &Pubkey,
94    system_info: &AccountInfo<'a>,
95    rent: &Rent,
96    extra_lamports: u64, // Extra lamports added on top of the rent exempt amount
97) -> Result<(), ProgramError> {
98    create_and_serialize_account_with_owner_signed(
99        payer_info,
100        account_info,
101        account_data,
102        account_address_seeds,
103        program_id,
104        program_id, // By default use PDA program_id as the owner of the account
105        system_info,
106        rent,
107        extra_lamports,
108    )
109}
110
111/// Creates a new account and serializes data into it using the provided seeds
112/// to invoke signed CPI call Note: This functions also checks the provided
113/// account PDA matches the supplied seeds
114#[allow(clippy::too_many_arguments)]
115pub fn create_and_serialize_account_with_owner_signed<'a, T: BorshSerialize + AccountMaxSize>(
116    payer_info: &AccountInfo<'a>,
117    account_info: &AccountInfo<'a>,
118    account_data: &T,
119    account_address_seeds: &[&[u8]],
120    program_id: &Pubkey,
121    owner_program_id: &Pubkey,
122    system_info: &AccountInfo<'a>,
123    rent: &Rent,
124    extra_lamports: u64, // Extra lamports added on top of the rent exempt amount
125) -> Result<(), ProgramError> {
126    // Get PDA and assert it's the same as the requested account address
127    let (account_address, bump_seed) =
128        Pubkey::find_program_address(account_address_seeds, program_id);
129
130    if account_address != *account_info.key {
131        msg!(
132            "Create account with PDA: {:?} was requested while PDA: {:?} was expected",
133            account_info.key,
134            account_address
135        );
136        return Err(ProgramError::InvalidSeeds);
137    }
138
139    let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() {
140        (None, max_size)
141    } else {
142        let serialized_data = account_data.try_to_vec()?;
143        let account_size = serialized_data.len();
144        (Some(serialized_data), account_size)
145    };
146
147    let mut signers_seeds = account_address_seeds.to_vec();
148    let bump = &[bump_seed];
149    signers_seeds.push(bump);
150
151    let rent_exempt_lamports = rent.minimum_balance(account_size);
152    let total_lamports = rent_exempt_lamports.checked_add(extra_lamports).unwrap();
153
154    // If the account has some lamports already it can't be created using
155    // create_account instruction.
156    // Anybody can send lamports to a PDA and by doing so create the account
157    // and perform DoS attack by blocking create_account
158    if account_info.lamports() > 0 {
159        let top_up_lamports = total_lamports.saturating_sub(account_info.lamports());
160
161        if top_up_lamports > 0 {
162            invoke(
163                &system_instruction::transfer(payer_info.key, account_info.key, top_up_lamports),
164                &[
165                    payer_info.clone(),
166                    account_info.clone(),
167                    system_info.clone(),
168                ],
169            )?;
170        }
171
172        invoke_signed(
173            &system_instruction::allocate(account_info.key, account_size as u64),
174            &[account_info.clone(), system_info.clone()],
175            &[&signers_seeds[..]],
176        )?;
177
178        invoke_signed(
179            &system_instruction::assign(account_info.key, owner_program_id),
180            &[account_info.clone(), system_info.clone()],
181            &[&signers_seeds[..]],
182        )?;
183    } else {
184        // If the PDA doesn't exist use create_account to use lower compute budget
185        let create_account_instruction = create_account(
186            payer_info.key,
187            account_info.key,
188            total_lamports,
189            account_size as u64,
190            owner_program_id,
191        );
192
193        invoke_signed(
194            &create_account_instruction,
195            &[
196                payer_info.clone(),
197                account_info.clone(),
198                system_info.clone(),
199            ],
200            &[&signers_seeds[..]],
201        )?;
202    }
203
204    if let Some(serialized_data) = serialized_data {
205        account_info
206            .data
207            .borrow_mut()
208            .copy_from_slice(&serialized_data);
209    } else if account_size > 0 {
210        borsh::to_writer(&mut account_info.data.borrow_mut()[..], account_data)?;
211    }
212
213    Ok(())
214}
215
216/// Deserializes account and checks it's initialized and owned by the specified
217/// program
218pub fn get_account_data<T: BorshDeserialize + IsInitialized>(
219    owner_program_id: &Pubkey,
220    account_info: &AccountInfo,
221) -> Result<T, ProgramError> {
222    if account_info.data_is_empty() {
223        return Err(GovernanceToolsError::AccountDoesNotExist.into());
224    }
225    if account_info.owner != owner_program_id {
226        return Err(GovernanceToolsError::InvalidAccountOwner.into());
227    }
228
229    let account: T = try_from_slice_unchecked(&account_info.data.borrow())?;
230    if !account.is_initialized() {
231        Err(ProgramError::UninitializedAccount)
232    } else {
233        Ok(account)
234    }
235}
236
237/// Deserializes account type and checks if the given account_info is owned by
238/// owner_program_id
239pub fn get_account_type<T: BorshDeserialize>(
240    owner_program_id: &Pubkey,
241    account_info: &AccountInfo,
242) -> Result<T, ProgramError> {
243    if account_info.data_is_empty() {
244        return Err(GovernanceToolsError::AccountDoesNotExist.into());
245    }
246
247    if account_info.owner != owner_program_id {
248        return Err(GovernanceToolsError::InvalidAccountOwner.into());
249    }
250
251    let account_type: T = try_from_slice_unchecked(&account_info.data.borrow())?;
252
253    Ok(account_type)
254}
255
256/// Asserts the given account is not empty, owned by the given program and of
257/// the expected type Note: The function assumes the account type T is stored as
258/// the first element in the account data
259pub fn assert_is_valid_account_of_type<T: BorshDeserialize + PartialEq>(
260    owner_program_id: &Pubkey,
261    account_info: &AccountInfo,
262    account_type: T,
263) -> Result<(), ProgramError> {
264    assert_is_valid_account_of_types(owner_program_id, account_info, |at: &T| *at == account_type)
265}
266
267/// Asserts the given account is not empty, owned by the given program and one
268/// of the types asserted via the provided predicate function Note: The function
269/// assumes the account type T is stored as the first element in the account
270/// data
271pub fn assert_is_valid_account_of_types<T: BorshDeserialize + PartialEq, F: Fn(&T) -> bool>(
272    owner_program_id: &Pubkey,
273    account_info: &AccountInfo,
274    is_account_type: F,
275) -> Result<(), ProgramError> {
276    if account_info.data_is_empty() {
277        return Err(GovernanceToolsError::AccountDoesNotExist.into());
278    }
279    if account_info.owner != owner_program_id {
280        return Err(GovernanceToolsError::InvalidAccountOwner.into());
281    }
282
283    let account_type: T = try_from_slice_unchecked(&account_info.data.borrow())?;
284
285    if !is_account_type(&account_type) {
286        return Err(GovernanceToolsError::InvalidAccountType.into());
287    };
288
289    Ok(())
290}
291
292/// Disposes account by transferring its lamports to the beneficiary account,
293/// resizing data to 0 and changing program owner to SystemProgram
294// After transaction completes the runtime would remove the account with no
295// lamports
296pub fn dispose_account(
297    account_info: &AccountInfo,
298    beneficiary_info: &AccountInfo,
299) -> Result<(), ProgramError> {
300    let account_lamports = account_info.lamports();
301    **account_info.lamports.borrow_mut() = 0;
302
303    **beneficiary_info.lamports.borrow_mut() = beneficiary_info
304        .lamports()
305        .checked_add(account_lamports)
306        .unwrap();
307
308    account_info.assign(&system_program::id());
309    account_info.realloc(0, false)
310}
311
312/// Extends account size to the new account size
313pub fn extend_account_size<'a>(
314    account_info: &AccountInfo<'a>,
315    payer_info: &AccountInfo<'a>,
316    new_account_size: usize,
317    rent: &Rent,
318    system_info: &AccountInfo<'a>,
319) -> Result<(), ProgramError> {
320    if new_account_size <= account_info.data_len() {
321        return Err(GovernanceToolsError::InvalidNewAccountSize.into());
322    }
323
324    let rent_exempt_lamports = rent.minimum_balance(new_account_size);
325    let top_up_lamports = rent_exempt_lamports.saturating_sub(account_info.lamports());
326
327    if top_up_lamports > 0 {
328        invoke(
329            &system_instruction::transfer(payer_info.key, account_info.key, top_up_lamports),
330            &[
331                payer_info.clone(),
332                account_info.clone(),
333                system_info.clone(),
334            ],
335        )?;
336    }
337
338    account_info.realloc(new_account_size, false)
339}