spl_token/
state.rs

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