light_compressed_token/
freeze.rs

1use account_compression::StateMerkleTreeAccount;
2use anchor_lang::prelude::*;
3use light_compressed_account::{
4    compressed_account::{CompressedAccount, CompressedAccountData},
5    hash_to_bn254_field_size_be,
6    instruction_data::{
7        compressed_proof::CompressedProof, cpi_context::CompressedCpiContext,
8        data::OutputCompressedAccountWithPackedContext, with_readonly::InAccount,
9    },
10};
11
12use crate::{
13    constants::TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR,
14    process_transfer::{
15        add_data_hash_to_input_compressed_accounts, cpi_execute_compressed_transaction_transfer,
16        get_input_compressed_accounts_with_merkle_context_and_check_signer,
17        InputTokenDataWithContext, BATCHED_DISCRIMINATOR,
18    },
19    token_data::{AccountState, TokenData},
20    FreezeInstruction,
21};
22
23#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
24pub struct CompressedTokenInstructionDataFreeze {
25    pub proof: CompressedProof,
26    pub owner: Pubkey,
27    pub input_token_data_with_context: Vec<InputTokenDataWithContext>,
28    pub cpi_context: Option<CompressedCpiContext>,
29    pub outputs_merkle_tree_index: u8,
30}
31
32pub fn process_freeze_or_thaw<
33    'a,
34    'b,
35    'c,
36    'info: 'b + 'c,
37    const FROZEN_INPUTS: bool,
38    const FROZEN_OUTPUTS: bool,
39>(
40    ctx: Context<'a, 'b, 'c, 'info, FreezeInstruction<'info>>,
41    inputs: Vec<u8>,
42) -> Result<()> {
43    let inputs: CompressedTokenInstructionDataFreeze =
44        CompressedTokenInstructionDataFreeze::deserialize(&mut inputs.as_slice())?;
45    let (compressed_input_accounts, output_compressed_accounts) =
46        create_input_and_output_accounts_freeze_or_thaw::<FROZEN_INPUTS, FROZEN_OUTPUTS>(
47            &inputs,
48            &ctx.accounts.mint.key(),
49            ctx.remaining_accounts,
50        )?;
51    // TODO: discuss
52    let proof = if inputs.proof == CompressedProof::default() {
53        None
54    } else {
55        Some(inputs.proof)
56    };
57    cpi_execute_compressed_transaction_transfer(
58        ctx.accounts,
59        compressed_input_accounts,
60        output_compressed_accounts,
61        false,
62        proof,
63        inputs.cpi_context,
64        ctx.accounts.cpi_authority_pda.to_account_info(),
65        ctx.accounts.light_system_program.to_account_info(),
66        ctx.accounts.self_program.to_account_info(),
67        ctx.remaining_accounts,
68    )
69}
70
71pub fn create_input_and_output_accounts_freeze_or_thaw<
72    const FROZEN_INPUTS: bool,
73    const FROZEN_OUTPUTS: bool,
74>(
75    inputs: &CompressedTokenInstructionDataFreeze,
76    mint: &Pubkey,
77    remaining_accounts: &[AccountInfo<'_>],
78) -> Result<(
79    Vec<InAccount>,
80    Vec<OutputCompressedAccountWithPackedContext>,
81)> {
82    if inputs.input_token_data_with_context.is_empty() {
83        return err!(crate::ErrorCode::NoInputTokenAccountsProvided);
84    }
85    let (mut compressed_input_accounts, input_token_data, _) =
86        get_input_compressed_accounts_with_merkle_context_and_check_signer::<FROZEN_INPUTS>(
87            // The signer in this case is the freeze authority. The owner is not
88            // required to sign for this instruction. Hence, we pass the owner
89            // from a variable instead of an account to still reproduce value
90            // token data hashes for the input accounts.
91            &inputs.owner,
92            &None,
93            remaining_accounts,
94            &inputs.input_token_data_with_context,
95            mint,
96        )?;
97    let output_len = compressed_input_accounts.len();
98    let mut output_compressed_accounts =
99        vec![OutputCompressedAccountWithPackedContext::default(); output_len];
100    let hashed_mint = hash_to_bn254_field_size_be(mint.to_bytes().as_slice());
101    create_token_output_accounts::<FROZEN_OUTPUTS>(
102        inputs.input_token_data_with_context.as_slice(),
103        remaining_accounts,
104        mint,
105        // The signer in this case is the freeze authority. The owner is not
106        // required to sign for this instruction. Hence, we pass the owner
107        // from a variable instead of an account to still reproduce value
108        // token data hashes for the input accounts.
109        &inputs.owner,
110        &inputs.outputs_merkle_tree_index,
111        &mut output_compressed_accounts,
112    )?;
113
114    add_data_hash_to_input_compressed_accounts::<FROZEN_INPUTS>(
115        &mut compressed_input_accounts,
116        input_token_data.as_slice(),
117        &hashed_mint,
118        remaining_accounts,
119    )?;
120    Ok((compressed_input_accounts, output_compressed_accounts))
121}
122
123/// This is a separate function from create_output_compressed_accounts to allow
124/// for a flexible number of delegates. create_output_compressed_accounts only
125/// supports one delegate.
126fn create_token_output_accounts<const IS_FROZEN: bool>(
127    input_token_data_with_context: &[InputTokenDataWithContext],
128    remaining_accounts: &[AccountInfo],
129    mint: &Pubkey,
130    owner: &Pubkey,
131    outputs_merkle_tree_index: &u8,
132    output_compressed_accounts: &mut [OutputCompressedAccountWithPackedContext],
133) -> Result<()> {
134    for (i, token_data_with_context) in input_token_data_with_context.iter().enumerate() {
135        // 106/74 =
136        //      32      mint
137        // +    32      owner
138        // +    8       amount
139        // +    1 + 32  option + delegate (optional)
140        // +    1       state
141        // +    1       tlv
142        let capacity = if token_data_with_context.delegate_index.is_some() {
143            107
144        } else {
145            75
146        };
147        let mut token_data_bytes = Vec::with_capacity(capacity);
148        let delegate = token_data_with_context
149            .delegate_index
150            .map(|index| remaining_accounts[index as usize].key());
151        let state = if IS_FROZEN {
152            AccountState::Frozen
153        } else {
154            AccountState::Initialized
155        };
156        // 1,000 CU token data and serialize
157        let token_data = TokenData {
158            mint: *mint,
159            owner: *owner,
160            amount: token_data_with_context.amount,
161            delegate,
162            state,
163            tlv: None,
164        };
165        token_data.serialize(&mut token_data_bytes)?;
166
167        let discriminator_bytes = &remaining_accounts[token_data_with_context
168            .merkle_context
169            .merkle_tree_pubkey_index
170            as usize]
171            .try_borrow_data()?[0..8];
172        use anchor_lang::Discriminator;
173        let data_hash = match discriminator_bytes {
174            StateMerkleTreeAccount::DISCRIMINATOR => token_data.hash_legacy(),
175            BATCHED_DISCRIMINATOR => token_data.hash(),
176            _ => panic!(),
177        }
178        .map_err(ProgramError::from)?;
179
180        let data: CompressedAccountData = CompressedAccountData {
181            discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR,
182            data: token_data_bytes,
183            data_hash,
184        };
185        output_compressed_accounts[i] = OutputCompressedAccountWithPackedContext {
186            compressed_account: CompressedAccount {
187                owner: crate::ID.into(),
188                lamports: token_data_with_context.lamports.unwrap_or(0),
189                data: Some(data),
190                address: None,
191            },
192            merkle_tree_index: *outputs_merkle_tree_index,
193        };
194    }
195    Ok(())
196}
197
198#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)]
199pub struct CompressedTokenInstructionDataThaw {
200    pub proof: CompressedProof,
201    pub owner: Pubkey,
202    pub input_token_data_with_context: Vec<InputTokenDataWithContext>,
203    pub cpi_context: Option<CompressedCpiContext>,
204    pub outputs_merkle_tree_index: u8,
205}
206
207#[cfg(not(target_os = "solana"))]
208pub mod sdk {
209
210    use anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas};
211    use light_compressed_account::{
212        compressed_account::{CompressedAccount, MerkleContext},
213        instruction_data::compressed_proof::CompressedProof,
214    };
215    use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
216
217    use super::CompressedTokenInstructionDataFreeze;
218    use crate::{
219        process_transfer::transfer_sdk::{
220            create_input_output_and_remaining_accounts, to_account_metas, TransferSdkError,
221        },
222        token_data::TokenData,
223    };
224
225    pub struct CreateInstructionInputs {
226        pub fee_payer: Pubkey,
227        pub authority: Pubkey,
228        pub root_indices: Vec<Option<u16>>,
229        pub proof: CompressedProof,
230        pub input_token_data: Vec<TokenData>,
231        pub input_compressed_accounts: Vec<CompressedAccount>,
232        pub input_merkle_contexts: Vec<MerkleContext>,
233        pub outputs_merkle_tree: Pubkey,
234    }
235
236    pub fn create_instruction<const FREEZE: bool>(
237        inputs: CreateInstructionInputs,
238    ) -> Result<Instruction, TransferSdkError> {
239        let (remaining_accounts, input_token_data_with_context, _) =
240            create_input_output_and_remaining_accounts(
241                &[inputs.outputs_merkle_tree],
242                &inputs.input_token_data,
243                &inputs.input_compressed_accounts,
244                &inputs.input_merkle_contexts,
245                &inputs.root_indices,
246                &Vec::new(),
247            );
248        let outputs_merkle_tree_index =
249            remaining_accounts.get(&inputs.outputs_merkle_tree).unwrap();
250
251        let inputs_struct = CompressedTokenInstructionDataFreeze {
252            proof: inputs.proof,
253            input_token_data_with_context,
254            cpi_context: None,
255            outputs_merkle_tree_index: *outputs_merkle_tree_index as u8,
256            owner: inputs.input_token_data[0].owner,
257        };
258        let remaining_accounts = to_account_metas(remaining_accounts);
259        let mut serialized_ix_data = Vec::new();
260        CompressedTokenInstructionDataFreeze::serialize(&inputs_struct, &mut serialized_ix_data)
261            .unwrap();
262
263        let (cpi_authority_pda, _) = crate::process_transfer::get_cpi_authority_pda();
264        let data = if FREEZE {
265            crate::instruction::Freeze {
266                inputs: serialized_ix_data,
267            }
268            .data()
269        } else {
270            crate::instruction::Thaw {
271                inputs: serialized_ix_data,
272            }
273            .data()
274        };
275
276        let accounts = crate::accounts::FreezeInstruction {
277            fee_payer: inputs.fee_payer,
278            authority: inputs.authority,
279            cpi_authority_pda,
280            light_system_program: light_system_program::ID,
281            registered_program_pda: light_system_program::utils::get_registered_program_pda(
282                &light_system_program::ID,
283            ),
284            noop_program: Pubkey::new_from_array(
285                account_compression::utils::constants::NOOP_PUBKEY,
286            ),
287            account_compression_authority: light_system_program::utils::get_cpi_authority_pda(
288                &light_system_program::ID,
289            ),
290            account_compression_program: account_compression::ID,
291            self_program: crate::ID,
292            system_program: solana_sdk::system_program::ID,
293            mint: inputs.input_token_data[0].mint,
294        };
295
296        Ok(Instruction {
297            program_id: crate::ID,
298            accounts: [accounts.to_account_metas(Some(true)), remaining_accounts].concat(),
299
300            data,
301        })
302    }
303
304    pub fn create_freeze_instruction(
305        inputs: CreateInstructionInputs,
306    ) -> Result<Instruction, TransferSdkError> {
307        create_instruction::<true>(inputs)
308    }
309
310    pub fn create_thaw_instruction(
311        inputs: CreateInstructionInputs,
312    ) -> Result<Instruction, TransferSdkError> {
313        create_instruction::<false>(inputs)
314    }
315}
316
317#[cfg(test)]
318pub mod test_freeze {
319    use account_compression::StateMerkleTreeAccount;
320    use anchor_lang::{solana_program::account_info::AccountInfo, Discriminator};
321    use light_compressed_account::compressed_account::PackedMerkleContext;
322    use rand::Rng;
323
324    use super::*;
325    use crate::{
326        constants::TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR, token_data::AccountState, TokenData,
327    };
328
329    // TODO: add randomized and edge case tests
330    #[test]
331    fn test_freeze() {
332        let merkle_tree_pubkey = Pubkey::new_unique();
333        let mut merkle_tree_account_lamports = 0;
334        let mut merkle_tree_account_data = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
335        let nullifier_queue_pubkey = Pubkey::new_unique();
336        let mut nullifier_queue_account_lamports = 0;
337        let mut nullifier_queue_account_data = Vec::new();
338        let delegate = Pubkey::new_unique();
339        let mut delegate_account_lamports = 0;
340        let mut delegate_account_data = Vec::new();
341        let merkle_tree_pubkey_1 = Pubkey::new_unique();
342        let mut merkle_tree_account_lamports_1 = 0;
343        let mut merkle_tree_account_data_1 = StateMerkleTreeAccount::DISCRIMINATOR.to_vec();
344        let remaining_accounts = vec![
345            AccountInfo::new(
346                &merkle_tree_pubkey,
347                false,
348                false,
349                &mut merkle_tree_account_lamports,
350                &mut merkle_tree_account_data,
351                &account_compression::ID,
352                false,
353                0,
354            ),
355            AccountInfo::new(
356                &nullifier_queue_pubkey,
357                false,
358                false,
359                &mut nullifier_queue_account_lamports,
360                &mut nullifier_queue_account_data,
361                &account_compression::ID,
362                false,
363                0,
364            ),
365            AccountInfo::new(
366                &delegate,
367                false,
368                false,
369                &mut delegate_account_lamports,
370                &mut delegate_account_data,
371                &account_compression::ID,
372                false,
373                0,
374            ),
375            AccountInfo::new(
376                &merkle_tree_pubkey_1,
377                false,
378                false,
379                &mut merkle_tree_account_lamports_1,
380                &mut merkle_tree_account_data_1,
381                &account_compression::ID,
382                false,
383                0,
384            ),
385        ];
386        let owner = Pubkey::new_unique();
387        let mint = Pubkey::new_unique();
388
389        let input_token_data_with_context = vec![
390            InputTokenDataWithContext {
391                amount: 100,
392
393                merkle_context: PackedMerkleContext {
394                    merkle_tree_pubkey_index: 0,
395                    queue_pubkey_index: 1,
396                    leaf_index: 1,
397                    prove_by_index: false,
398                },
399                root_index: 0,
400                delegate_index: None,
401                lamports: None,
402                tlv: None,
403            },
404            InputTokenDataWithContext {
405                amount: 101,
406
407                merkle_context: PackedMerkleContext {
408                    merkle_tree_pubkey_index: 0,
409                    queue_pubkey_index: 1,
410                    leaf_index: 2,
411                    prove_by_index: false,
412                },
413                root_index: 0,
414                delegate_index: Some(2),
415                lamports: None,
416                tlv: None,
417            },
418        ];
419        // Freeze
420        {
421            let inputs = CompressedTokenInstructionDataFreeze {
422                proof: CompressedProof::default(),
423                owner,
424                input_token_data_with_context: input_token_data_with_context.clone(),
425                cpi_context: None,
426                outputs_merkle_tree_index: 3,
427            };
428            let (compressed_input_accounts, output_compressed_accounts) =
429                create_input_and_output_accounts_freeze_or_thaw::<false, true>(
430                    &inputs,
431                    &mint,
432                    &remaining_accounts,
433                )
434                .unwrap();
435            assert_eq!(compressed_input_accounts.len(), 2);
436            assert_eq!(output_compressed_accounts.len(), 2);
437            let expected_change_token_data = TokenData {
438                mint,
439                owner,
440                amount: 100,
441                delegate: None,
442                state: AccountState::Frozen,
443                tlv: None,
444            };
445            let expected_delegated_token_data = TokenData {
446                mint,
447                owner,
448                amount: 101,
449                delegate: Some(delegate),
450                state: AccountState::Frozen,
451                tlv: None,
452            };
453
454            let expected_compressed_output_accounts = create_expected_token_output_accounts(
455                vec![expected_change_token_data, expected_delegated_token_data],
456                vec![3u8; 2],
457            );
458            assert_eq!(
459                output_compressed_accounts,
460                expected_compressed_output_accounts
461            );
462        }
463        // Thaw
464        {
465            let inputs = CompressedTokenInstructionDataFreeze {
466                proof: CompressedProof::default(),
467                owner,
468                input_token_data_with_context,
469                cpi_context: None,
470                outputs_merkle_tree_index: 3,
471            };
472            let (compressed_input_accounts, output_compressed_accounts) =
473                create_input_and_output_accounts_freeze_or_thaw::<true, false>(
474                    &inputs,
475                    &mint,
476                    &remaining_accounts,
477                )
478                .unwrap();
479            assert_eq!(compressed_input_accounts.len(), 2);
480            assert_eq!(output_compressed_accounts.len(), 2);
481            let expected_change_token_data = TokenData {
482                mint,
483                owner,
484                amount: 100,
485                delegate: None,
486                state: AccountState::Initialized,
487                tlv: None,
488            };
489            let expected_delegated_token_data = TokenData {
490                mint,
491                owner,
492                amount: 101,
493                delegate: Some(delegate),
494                state: AccountState::Initialized,
495                tlv: None,
496            };
497
498            let expected_compressed_output_accounts = create_expected_token_output_accounts(
499                vec![expected_change_token_data, expected_delegated_token_data],
500                vec![3u8; 2],
501            );
502            assert_eq!(
503                output_compressed_accounts,
504                expected_compressed_output_accounts
505            );
506        }
507    }
508
509    pub fn create_expected_token_output_accounts(
510        expected_token_data: Vec<TokenData>,
511        merkle_tree_indices: Vec<u8>,
512    ) -> Vec<OutputCompressedAccountWithPackedContext> {
513        let mut expected_compressed_output_accounts = Vec::new();
514        for (token_data, merkle_tree_index) in
515            expected_token_data.iter().zip(merkle_tree_indices.iter())
516        {
517            let serialized_expected_token_data = token_data.try_to_vec().unwrap();
518            let change_data_struct = CompressedAccountData {
519                discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR,
520                data: serialized_expected_token_data.clone(),
521                data_hash: token_data.hash_legacy().unwrap(),
522            };
523            expected_compressed_output_accounts.push(OutputCompressedAccountWithPackedContext {
524                compressed_account: CompressedAccount {
525                    owner: crate::ID.into(),
526                    lamports: 0,
527                    data: Some(change_data_struct),
528                    address: None,
529                },
530                merkle_tree_index: *merkle_tree_index,
531            });
532        }
533        expected_compressed_output_accounts
534    }
535    pub fn get_rnd_input_token_data_with_contexts(
536        rng: &mut rand::rngs::ThreadRng,
537        num: usize,
538    ) -> Vec<InputTokenDataWithContext> {
539        let mut vec = Vec::with_capacity(num);
540        for _ in 0..num {
541            let delegate_index = if rng.gen_bool(0.5) { Some(1) } else { None };
542            vec.push(InputTokenDataWithContext {
543                amount: rng.gen_range(0..1_000_000_000),
544                merkle_context: PackedMerkleContext {
545                    merkle_tree_pubkey_index: 0,
546                    queue_pubkey_index: 1,
547                    leaf_index: rng.gen_range(0..1_000_000_000),
548                    prove_by_index: false,
549                },
550                root_index: rng.gen_range(0..=65_535),
551                delegate_index,
552                lamports: None,
553                tlv: None,
554            });
555        }
556        vec
557    }
558    pub fn create_expected_input_accounts(
559        input_token_data_with_context: &[InputTokenDataWithContext],
560        mint: &Pubkey,
561        owner: &Pubkey,
562        remaining_accounts: &[Pubkey],
563    ) -> Vec<InAccount> {
564        input_token_data_with_context
565            .iter()
566            .map(|x| {
567                let delegate = x
568                    .delegate_index
569                    .map(|index| remaining_accounts[index as usize]);
570                let token_data = TokenData {
571                    mint: *mint,
572                    owner: *owner,
573                    amount: x.amount,
574                    delegate,
575                    state: AccountState::Initialized,
576                    tlv: None,
577                };
578                let mut data = Vec::new();
579                token_data.serialize(&mut data).unwrap();
580                let data_hash = token_data.hash_legacy().unwrap();
581                InAccount {
582                    lamports: 0,
583                    address: None,
584                    data_hash,
585                    discriminator: TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR,
586                    root_index: x.root_index,
587                    merkle_context: x.merkle_context,
588                }
589            })
590            .collect()
591    }
592}