Skip to main content

apl_token/
state.rs

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