safe_token/
state.rs

1//! State transition types
2
3use crate::instruction::MAX_SIGNERS;
4use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
5use num_enum::TryFromPrimitive;
6use solana_program::{
7    program_error::ProgramError,
8    program_option::COption,
9    program_pack::{IsInitialized, Pack, Sealed},
10    pubkey::{Pubkey, PUBKEY_BYTES},
11};
12
13/// Mint data.
14#[repr(C)]
15#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct Mint {
17    /// Optional authority used to mint new tokens. The mint authority may only be provided during
18    /// mint creation. If no mint authority is present then the mint has a fixed supply and no
19    /// further tokens may be minted.
20    pub mint_authority: COption<Pubkey>,
21    /// Total supply of tokens.
22    pub supply: u64,
23    /// Number of base 10 digits to the right of the decimal place.
24    pub decimals: u8,
25    /// Is `true` if this structure has been initialized
26    pub is_initialized: bool,
27    /// Optional authority to freeze token accounts.
28    pub freeze_authority: COption<Pubkey>,
29}
30impl Sealed for Mint {}
31impl IsInitialized for Mint {
32    fn is_initialized(&self) -> bool {
33        self.is_initialized
34    }
35}
36impl Pack for Mint {
37    const LEN: usize = 82;
38    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
39        let src = array_ref![src, 0, 82];
40        let (mint_authority, supply, decimals, is_initialized, freeze_authority) =
41            array_refs![src, 36, 8, 1, 1, 36];
42        let mint_authority = unpack_coption_key(mint_authority)?;
43        let supply = u64::from_le_bytes(*supply);
44        let decimals = decimals[0];
45        let is_initialized = match is_initialized {
46            [0] => false,
47            [1] => true,
48            _ => return Err(ProgramError::InvalidAccountData),
49        };
50        let freeze_authority = unpack_coption_key(freeze_authority)?;
51        Ok(Mint {
52            mint_authority,
53            supply,
54            decimals,
55            is_initialized,
56            freeze_authority,
57        })
58    }
59    fn pack_into_slice(&self, dst: &mut [u8]) {
60        let dst = array_mut_ref![dst, 0, 82];
61        let (
62            mint_authority_dst,
63            supply_dst,
64            decimals_dst,
65            is_initialized_dst,
66            freeze_authority_dst,
67        ) = mut_array_refs![dst, 36, 8, 1, 1, 36];
68        let &Mint {
69            ref mint_authority,
70            supply,
71            decimals,
72            is_initialized,
73            ref freeze_authority,
74        } = self;
75        pack_coption_key(mint_authority, mint_authority_dst);
76        *supply_dst = supply.to_le_bytes();
77        decimals_dst[0] = decimals;
78        is_initialized_dst[0] = is_initialized as u8;
79        pack_coption_key(freeze_authority, freeze_authority_dst);
80    }
81}
82
83/// Account data.
84#[repr(C)]
85#[derive(Clone, Copy, Debug, Default, PartialEq)]
86pub struct Account {
87    /// The mint associated with this account
88    pub mint: Pubkey,
89    /// The owner of this account.
90    pub owner: Pubkey,
91    /// The amount of tokens this account holds.
92    pub amount: u64,
93    /// If `delegate` is `Some` then `delegated_amount` represents
94    /// the amount authorized by the delegate
95    pub delegate: COption<Pubkey>,
96    /// The account's state
97    pub state: AccountState,
98    /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
99    /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
100    /// wrapped SAFE accounts do not drop below this threshold.
101    pub is_native: COption<u64>,
102    /// The amount delegated
103    pub delegated_amount: u64,
104    /// Optional authority to close the account.
105    pub close_authority: COption<Pubkey>,
106}
107impl Account {
108    /// Checks if account is frozen
109    pub fn is_frozen(&self) -> bool {
110        self.state == AccountState::Frozen
111    }
112    /// Checks if account is native
113    pub fn is_native(&self) -> bool {
114        self.is_native.is_some()
115    }
116    /// Checks if a token Account's owner is the system_program or the incinerator
117    pub fn is_owned_by_system_program_or_incinerator(&self) -> bool {
118        solana_program::system_program::check_id(&self.owner)
119            || solana_program::incinerator::check_id(&self.owner)
120    }
121}
122impl Sealed for Account {}
123impl IsInitialized for Account {
124    fn is_initialized(&self) -> bool {
125        self.state != AccountState::Uninitialized
126    }
127}
128impl Pack for Account {
129    const LEN: usize = 165;
130    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
131        let src = array_ref![src, 0, 165];
132        let (mint, owner, amount, delegate, state, is_native, delegated_amount, close_authority) =
133            array_refs![src, 32, 32, 8, 36, 1, 12, 8, 36];
134        Ok(Account {
135            mint: Pubkey::new_from_array(*mint),
136            owner: Pubkey::new_from_array(*owner),
137            amount: u64::from_le_bytes(*amount),
138            delegate: unpack_coption_key(delegate)?,
139            state: AccountState::try_from_primitive(state[0])
140                .or(Err(ProgramError::InvalidAccountData))?,
141            is_native: unpack_coption_u64(is_native)?,
142            delegated_amount: u64::from_le_bytes(*delegated_amount),
143            close_authority: unpack_coption_key(close_authority)?,
144        })
145    }
146    fn pack_into_slice(&self, dst: &mut [u8]) {
147        let dst = array_mut_ref![dst, 0, 165];
148        let (
149            mint_dst,
150            owner_dst,
151            amount_dst,
152            delegate_dst,
153            state_dst,
154            is_native_dst,
155            delegated_amount_dst,
156            close_authority_dst,
157        ) = mut_array_refs![dst, 32, 32, 8, 36, 1, 12, 8, 36];
158        let &Account {
159            ref mint,
160            ref owner,
161            amount,
162            ref delegate,
163            state,
164            ref is_native,
165            delegated_amount,
166            ref close_authority,
167        } = self;
168        mint_dst.copy_from_slice(mint.as_ref());
169        owner_dst.copy_from_slice(owner.as_ref());
170        *amount_dst = amount.to_le_bytes();
171        pack_coption_key(delegate, delegate_dst);
172        state_dst[0] = state as u8;
173        pack_coption_u64(is_native, is_native_dst);
174        *delegated_amount_dst = delegated_amount.to_le_bytes();
175        pack_coption_key(close_authority, close_authority_dst);
176    }
177}
178
179/// Account state.
180#[repr(u8)]
181#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive)]
182pub enum AccountState {
183    /// Account is not yet initialized
184    Uninitialized,
185    /// Account is initialized; the account owner and/or delegate may perform permitted operations
186    /// on this account
187    Initialized,
188    /// Account has been frozen by the mint freeze authority. Neither the account owner nor
189    /// the delegate are able to perform operations on this account.
190    Frozen,
191}
192
193impl Default for AccountState {
194    fn default() -> Self {
195        AccountState::Uninitialized
196    }
197}
198
199/// Multisignature data.
200#[repr(C)]
201#[derive(Clone, Copy, Debug, Default, PartialEq)]
202pub struct Multisig {
203    /// Number of signers required
204    pub m: u8,
205    /// Number of valid signers
206    pub n: u8,
207    /// Is `true` if this structure has been initialized
208    pub is_initialized: bool,
209    /// Signer public keys
210    pub signers: [Pubkey; MAX_SIGNERS],
211}
212impl Sealed for Multisig {}
213impl IsInitialized for Multisig {
214    fn is_initialized(&self) -> bool {
215        self.is_initialized
216    }
217}
218impl Pack for Multisig {
219    const LEN: usize = 355;
220    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
221        let src = array_ref![src, 0, 355];
222        #[allow(clippy::ptr_offset_with_cast)]
223        let (m, n, is_initialized, signers_flat) = array_refs![src, 1, 1, 1, 32 * MAX_SIGNERS];
224        let mut result = Multisig {
225            m: m[0],
226            n: n[0],
227            is_initialized: match is_initialized {
228                [0] => false,
229                [1] => true,
230                _ => return Err(ProgramError::InvalidAccountData),
231            },
232            signers: [Pubkey::new_from_array([0u8; 32]); MAX_SIGNERS],
233        };
234        for (src, dst) in signers_flat.chunks(32).zip(result.signers.iter_mut()) {
235            *dst = Pubkey::new(src);
236        }
237        Ok(result)
238    }
239    fn pack_into_slice(&self, dst: &mut [u8]) {
240        let dst = array_mut_ref![dst, 0, 355];
241        #[allow(clippy::ptr_offset_with_cast)]
242        let (m, n, is_initialized, signers_flat) = mut_array_refs![dst, 1, 1, 1, 32 * MAX_SIGNERS];
243        *m = [self.m];
244        *n = [self.n];
245        *is_initialized = [self.is_initialized as u8];
246        for (i, src) in self.signers.iter().enumerate() {
247            let dst_array = array_mut_ref![signers_flat, 32 * i, 32];
248            dst_array.copy_from_slice(src.as_ref());
249        }
250    }
251}
252
253// Helpers
254fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
255    let (tag, body) = mut_array_refs![dst, 4, 32];
256    match src {
257        COption::Some(key) => {
258            *tag = [1, 0, 0, 0];
259            body.copy_from_slice(key.as_ref());
260        }
261        COption::None => {
262            *tag = [0; 4];
263        }
264    }
265}
266fn unpack_coption_key(src: &[u8; 36]) -> Result<COption<Pubkey>, ProgramError> {
267    let (tag, body) = array_refs![src, 4, 32];
268    match *tag {
269        [0, 0, 0, 0] => Ok(COption::None),
270        [1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))),
271        _ => Err(ProgramError::InvalidAccountData),
272    }
273}
274fn pack_coption_u64(src: &COption<u64>, dst: &mut [u8; 12]) {
275    let (tag, body) = mut_array_refs![dst, 4, 8];
276    match src {
277        COption::Some(amount) => {
278            *tag = [1, 0, 0, 0];
279            *body = amount.to_le_bytes();
280        }
281        COption::None => {
282            *tag = [0; 4];
283        }
284    }
285}
286fn unpack_coption_u64(src: &[u8; 12]) -> Result<COption<u64>, ProgramError> {
287    let (tag, body) = array_refs![src, 4, 8];
288    match *tag {
289        [0, 0, 0, 0] => Ok(COption::None),
290        [1, 0, 0, 0] => Ok(COption::Some(u64::from_le_bytes(*body))),
291        _ => Err(ProgramError::InvalidAccountData),
292    }
293}
294
295const SAFE_TOKEN_ACCOUNT_MINT_OFFSET: usize = 0;
296const SAFE_TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32;
297
298/// A trait for token Account structs to enable efficiently unpacking various fields
299/// without unpacking the complete state.
300pub trait GenericTokenAccount {
301    /// Check if the account data is a valid token account
302    fn valid_account_data(account_data: &[u8]) -> bool;
303
304    /// Call after account length has already been verified to unpack the account owner
305    fn unpack_account_owner_unchecked(account_data: &[u8]) -> &Pubkey {
306        Self::unpack_pubkey_unchecked(account_data, SAFE_TOKEN_ACCOUNT_OWNER_OFFSET)
307    }
308
309    /// Call after account length has already been verified to unpack the account mint
310    fn unpack_account_mint_unchecked(account_data: &[u8]) -> &Pubkey {
311        Self::unpack_pubkey_unchecked(account_data, SAFE_TOKEN_ACCOUNT_MINT_OFFSET)
312    }
313
314    /// Call after account length has already been verified to unpack a Pubkey at
315    /// the specified offset. Panics if `account_data.len()` is less than `PUBKEY_BYTES`
316    fn unpack_pubkey_unchecked(account_data: &[u8], offset: usize) -> &Pubkey {
317        bytemuck::from_bytes(&account_data[offset..offset + PUBKEY_BYTES])
318    }
319
320    /// Unpacks an account's owner from opaque account data.
321    fn unpack_account_owner(account_data: &[u8]) -> Option<&Pubkey> {
322        if Self::valid_account_data(account_data) {
323            Some(Self::unpack_account_owner_unchecked(account_data))
324        } else {
325            None
326        }
327    }
328
329    /// Unpacks an account's mint from opaque account data.
330    fn unpack_account_mint(account_data: &[u8]) -> Option<&Pubkey> {
331        if Self::valid_account_data(account_data) {
332            Some(Self::unpack_account_mint_unchecked(account_data))
333        } else {
334            None
335        }
336    }
337}
338
339/// The offset of state field in Account's C representation
340pub const ACCOUNT_INITIALIZED_INDEX: usize = 108;
341
342/// Check if the account data buffer represents an initialized account.
343/// This is checking the `state` (AccountState) field of an Account object.
344pub fn is_initialized_account(account_data: &[u8]) -> bool {
345    *account_data
346        .get(ACCOUNT_INITIALIZED_INDEX)
347        .unwrap_or(&(AccountState::Uninitialized as u8))
348        != AccountState::Uninitialized as u8
349}
350
351impl GenericTokenAccount for Account {
352    fn valid_account_data(account_data: &[u8]) -> bool {
353        account_data.len() == Account::LEN && is_initialized_account(account_data)
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_mint_unpack_from_slice() {
363        let src: [u8; 82] = [0; 82];
364        let mint = Mint::unpack_from_slice(&src).unwrap();
365        assert!(!mint.is_initialized);
366
367        let mut src: [u8; 82] = [0; 82];
368        src[45] = 2;
369        let mint = Mint::unpack_from_slice(&src).unwrap_err();
370        assert_eq!(mint, ProgramError::InvalidAccountData);
371    }
372
373    #[test]
374    fn test_account_state() {
375        let account_state = AccountState::default();
376        assert_eq!(account_state, AccountState::Uninitialized);
377    }
378
379    #[test]
380    fn test_multisig_unpack_from_slice() {
381        let src: [u8; 355] = [0; 355];
382        let multisig = Multisig::unpack_from_slice(&src).unwrap();
383        assert_eq!(multisig.m, 0);
384        assert_eq!(multisig.n, 0);
385        assert!(!multisig.is_initialized);
386
387        let mut src: [u8; 355] = [0; 355];
388        src[0] = 1;
389        src[1] = 1;
390        src[2] = 1;
391        let multisig = Multisig::unpack_from_slice(&src).unwrap();
392        assert_eq!(multisig.m, 1);
393        assert_eq!(multisig.n, 1);
394        assert!(multisig.is_initialized);
395
396        let mut src: [u8; 355] = [0; 355];
397        src[2] = 2;
398        let multisig = Multisig::unpack_from_slice(&src).unwrap_err();
399        assert_eq!(multisig, ProgramError::InvalidAccountData);
400    }
401
402    #[test]
403    fn test_unpack_coption_key() {
404        let src: [u8; 36] = [0; 36];
405        let result = unpack_coption_key(&src).unwrap();
406        assert_eq!(result, COption::None);
407
408        let mut src: [u8; 36] = [0; 36];
409        src[1] = 1;
410        let result = unpack_coption_key(&src).unwrap_err();
411        assert_eq!(result, ProgramError::InvalidAccountData);
412    }
413
414    #[test]
415    fn test_unpack_coption_u64() {
416        let src: [u8; 12] = [0; 12];
417        let result = unpack_coption_u64(&src).unwrap();
418        assert_eq!(result, COption::None);
419
420        let mut src: [u8; 12] = [0; 12];
421        src[0] = 1;
422        let result = unpack_coption_u64(&src).unwrap();
423        assert_eq!(result, COption::Some(0));
424
425        let mut src: [u8; 12] = [0; 12];
426        src[1] = 1;
427        let result = unpack_coption_u64(&src).unwrap_err();
428        assert_eq!(result, ProgramError::InvalidAccountData);
429    }
430
431    #[test]
432    fn test_unpack_token_owner() {
433        // Account data length < Account::LEN, unpack will not return a key
434        let src: [u8; 12] = [0; 12];
435        let result = Account::unpack_account_owner(&src);
436        assert_eq!(result, Option::None);
437
438        // The right account data size and initialized, unpack will return some key
439        let mut src: [u8; Account::LEN] = [0; Account::LEN];
440        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8;
441        let result = Account::unpack_account_owner(&src);
442        assert!(result.is_some());
443
444        // The right account data size and frozen, unpack will return some key
445        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8;
446        let result = Account::unpack_account_owner(&src);
447        assert!(result.is_some());
448
449        // The right account data size and uninitialized, unpack will return None
450        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8;
451        let result = Account::unpack_account_mint(&src);
452        assert_eq!(result, Option::None);
453
454        // Account data length > account data size, unpack will not return a key
455        let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5];
456        let result = Account::unpack_account_owner(&src);
457        assert_eq!(result, Option::None);
458    }
459
460    #[test]
461    fn test_unpack_token_mint() {
462        // Account data length < Account::LEN, unpack will not return a key
463        let src: [u8; 12] = [0; 12];
464        let result = Account::unpack_account_mint(&src);
465        assert_eq!(result, Option::None);
466
467        // The right account data size and initialized, unpack will return some key
468        let mut src: [u8; Account::LEN] = [0; Account::LEN];
469        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Initialized as u8;
470        let result = Account::unpack_account_mint(&src);
471        assert!(result.is_some());
472
473        // The right account data size and frozen, unpack will return some key
474        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Frozen as u8;
475        let result = Account::unpack_account_mint(&src);
476        assert!(result.is_some());
477
478        // The right account data size and uninitialized, unpack will return None
479        src[ACCOUNT_INITIALIZED_INDEX] = AccountState::Uninitialized as u8;
480        let result = Account::unpack_account_mint(&src);
481        assert_eq!(result, Option::None);
482
483        // Account data length > account data size, unpack will not return a key
484        let src: [u8; Account::LEN + 5] = [0; Account::LEN + 5];
485        let result = Account::unpack_account_mint(&src);
486        assert_eq!(result, Option::None);
487    }
488}