1use super::{freeze_sol_payment::freeze_nft, *};
2
3use anchor_lang::AccountsClose;
4use mpl_token_metadata::state::{Metadata, TokenMetadataAccount};
5use solana_program::{
6 program::{invoke, invoke_signed},
7 program_pack::Pack,
8 system_instruction, system_program,
9};
10use spl_associated_token_account::{
11 get_associated_token_address, instruction::create_associated_token_account,
12};
13use spl_token::{instruction::close_account, state::Account as TokenAccount};
14
15use crate::{
16 errors::CandyGuardError,
17 guards::freeze_sol_payment::{initialize_freeze, thaw_nft, FREEZE_SOL_FEE},
18 instructions::Token,
19 state::GuardType,
20 utils::{
21 assert_initialized, assert_is_ata, assert_is_token_account, assert_keys_equal,
22 assert_owned_by, cmp_pubkeys, spl_token_transfer, TokenTransferParams,
23 },
24};
25
26#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
39pub struct FreezeTokenPayment {
40 pub amount: u64,
41 pub mint: Pubkey,
42 pub destination_ata: Pubkey,
43}
44
45impl Guard for FreezeTokenPayment {
46 fn size() -> usize {
47 8 + 32 + 32 }
51
52 fn mask() -> u64 {
53 GuardType::as_mask(GuardType::FreezeTokenPayment)
54 }
55
56 fn instruction<'info>(
62 ctx: &Context<'_, '_, '_, 'info, Route<'info>>,
63 route_context: RouteContext<'info>,
64 data: Vec<u8>,
65 ) -> Result<()> {
66 let instruction: FreezeInstruction =
68 if let Ok(instruction) = FreezeInstruction::try_from_slice(&data[0..1]) {
69 instruction
70 } else {
71 return err!(CandyGuardError::MissingFreezeInstruction);
72 };
73
74 match instruction {
75 FreezeInstruction::Initialize => {
89 msg!("Instruction: Initialize (FreezeTokenPayment guard)");
90
91 if route_context.candy_guard.is_none() || route_context.candy_machine.is_none() {
92 return err!(CandyGuardError::Uninitialized);
93 }
94
95 let (destination, mint) = if let Some(guard_set) = &route_context.guard_set {
96 if let Some(freeze_guard) = &guard_set.freeze_token_payment {
97 (freeze_guard.destination_ata, freeze_guard.mint)
98 } else {
99 return err!(CandyGuardError::FreezeGuardNotEnabled);
100 }
101 } else {
102 return err!(CandyGuardError::FreezeGuardNotEnabled);
103 };
104
105 initialize_freeze(ctx, route_context, data, destination)?;
108
109 let freeze_pda = try_get_account_info(ctx.remaining_accounts, 0)?;
112
113 let system_program = try_get_account_info(ctx.remaining_accounts, 2)?;
114 assert_keys_equal(system_program.key, &system_program::ID)?;
115
116 let freeze_ata = try_get_account_info(ctx.remaining_accounts, 3)?;
117 let token_mint = try_get_account_info(ctx.remaining_accounts, 4)?;
118 assert_keys_equal(token_mint.key, &mint)?;
119 let token_program = try_get_account_info(ctx.remaining_accounts, 5)?;
121 assert_keys_equal(token_program.key, &spl_token::ID)?;
122 let associate_token_program = try_get_account_info(ctx.remaining_accounts, 6)?;
124 assert_keys_equal(
125 associate_token_program.key,
126 &spl_associated_token_account::ID,
127 )?;
128
129 let destination_ata = try_get_account_info(ctx.remaining_accounts, 7)?;
130 assert_keys_equal(destination_ata.key, &destination)?;
131 let ata_account: spl_token::state::Account = assert_initialized(destination_ata)?;
132 assert_keys_equal(&ata_account.mint, &mint)?;
133
134 assert_keys_equal(
135 &get_associated_token_address(freeze_pda.key, token_mint.key),
136 freeze_ata.key,
137 )?;
138
139 invoke(
140 &create_associated_token_account(
141 ctx.accounts.payer.key,
142 freeze_pda.key,
143 token_mint.key,
144 &spl_token::ID,
145 ),
146 &[
147 ctx.accounts.payer.to_account_info(),
148 freeze_ata.to_account_info(),
149 freeze_pda.to_account_info(),
150 token_mint.to_account_info(),
151 system_program.to_account_info(),
152 ],
153 )?;
154
155 Ok(())
156 }
157 FreezeInstruction::Thaw => {
182 msg!("Instruction: Thaw (FreezeTokenPayment guard)");
183 thaw_nft(ctx, route_context, data)
184 }
185 FreezeInstruction::UnlockFunds => {
199 msg!("Instruction: Unlock Funds (FreezeTokenPayment guard)");
200 unlock_funds(ctx, route_context)
201 }
202 }
203 }
204}
205
206impl Condition for FreezeTokenPayment {
207 fn validate<'info>(
208 &self,
209 ctx: &mut EvaluationContext,
210 _guard_set: &GuardSet,
211 _mint_args: &[u8],
212 ) -> Result<()> {
213 let candy_guard_key = &ctx.accounts.candy_guard.key();
214 let candy_machine_key = &ctx.accounts.candy_machine.key();
215
216 let index = ctx.account_cursor;
219 let freeze_pda = try_get_account_info(ctx.accounts.remaining, index)?;
220 ctx.account_cursor += 1;
221
222 let seeds = [
223 FreezeEscrow::PREFIX_SEED,
224 self.destination_ata.as_ref(),
225 candy_guard_key.as_ref(),
226 candy_machine_key.as_ref(),
227 ];
228
229 let (pda, _) = Pubkey::find_program_address(&seeds, &crate::ID);
230 assert_keys_equal(freeze_pda.key, &pda)?;
231
232 if freeze_pda.data_is_empty() {
233 return err!(CandyGuardError::FreezeNotInitialized);
234 }
235
236 let nft_ata = try_get_account_info(ctx.accounts.remaining, index + 1)?;
237 ctx.account_cursor += 1;
238
239 if nft_ata.data_is_empty() {
240 let (derivation, _) = Pubkey::find_program_address(
244 &[
245 ctx.accounts.minter.key.as_ref(),
246 spl_token::id().as_ref(),
247 ctx.accounts.nft_mint.key.as_ref(),
248 ],
249 &spl_associated_token_account::id(),
250 );
251
252 assert_keys_equal(&derivation, nft_ata.key)?;
253 } else {
254 assert_is_token_account(nft_ata, ctx.accounts.minter.key, ctx.accounts.nft_mint.key)?;
256 }
257
258 if let Some(token_info) = &ctx.accounts.token {
260 assert_keys_equal(nft_ata.key, token_info.key)?;
261 }
262
263 let token_account_info = try_get_account_info(ctx.accounts.remaining, index + 2)?;
264 let destination_ata = try_get_account_info(ctx.accounts.remaining, index + 3)?;
266 assert_is_ata(destination_ata, &freeze_pda.key(), &self.mint)?;
267
268 ctx.account_cursor += 2;
269
270 let token_account =
271 assert_is_ata(token_account_info, &ctx.accounts.minter.key(), &self.mint)?;
272
273 if token_account.amount < self.amount {
274 return err!(CandyGuardError::NotEnoughTokens);
275 }
276
277 let candy_machine_info = ctx.accounts.candy_machine.to_account_info();
278 let account_data = candy_machine_info.data.borrow_mut();
279
280 let collection_metadata =
281 Metadata::from_account_info(&ctx.accounts.collection_metadata.to_account_info())?;
282
283 let rule_set = ctx
284 .accounts
285 .candy_machine
286 .get_rule_set(&account_data, &collection_metadata)?;
287
288 if let Some(rule_set) = rule_set {
289 let mint_rule_set = try_get_account_info(ctx.accounts.remaining, index + 4)?;
290 assert_keys_equal(mint_rule_set.key, &rule_set)?;
291 ctx.account_cursor += 1;
292 }
293
294 if ctx.accounts.payer.lamports() < FREEZE_SOL_FEE {
295 msg!(
296 "Require {} lamports, accounts has {} lamports",
297 FREEZE_SOL_FEE,
298 ctx.accounts.payer.lamports(),
299 );
300 return err!(CandyGuardError::NotEnoughSOL);
301 }
302
303 ctx.indices.insert("freeze_token_payment", index);
304
305 Ok(())
306 }
307
308 fn pre_actions<'info>(
309 &self,
310 ctx: &mut EvaluationContext,
311 _guard_set: &GuardSet,
312 _mint_args: &[u8],
313 ) -> Result<()> {
314 let index = ctx.indices["freeze_token_payment"];
315 let freeze_pda = try_get_account_info(ctx.accounts.remaining, index)?;
317 let token_account_info = try_get_account_info(ctx.accounts.remaining, index + 2)?;
318 let destination_ata = try_get_account_info(ctx.accounts.remaining, index + 3)?;
319
320 spl_token_transfer(TokenTransferParams {
321 source: token_account_info.to_account_info(),
322 destination: destination_ata.to_account_info(),
323 authority: ctx.accounts.minter.to_account_info(),
324 authority_signer_seeds: &[],
325 token_program: ctx.accounts.spl_token_program.to_account_info(),
326 amount: self.amount,
327 })?;
328
329 invoke(
330 &system_instruction::transfer(
331 &ctx.accounts.payer.key(),
332 &freeze_pda.key(),
333 FREEZE_SOL_FEE,
334 ),
335 &[
336 ctx.accounts.payer.to_account_info(),
337 freeze_pda.to_account_info(),
338 ctx.accounts.system_program.to_account_info(),
339 ],
340 )?;
341
342 Ok(())
343 }
344
345 fn post_actions<'info>(
346 &self,
347 ctx: &mut EvaluationContext,
348 _guard_set: &GuardSet,
349 _mint_args: &[u8],
350 ) -> Result<()> {
351 freeze_nft(
353 ctx,
354 ctx.indices["freeze_token_payment"],
355 &self.destination_ata,
356 4,
357 )
358 }
359}
360
361fn unlock_funds<'info>(
363 ctx: &Context<'_, '_, '_, 'info, Route<'info>>,
364 route_context: RouteContext<'info>,
365) -> Result<()> {
366 let candy_guard_key = &ctx.accounts.candy_guard.key();
367 let candy_machine_key = &ctx.accounts.candy_machine.key();
368
369 let freeze_pda = try_get_account_info(ctx.remaining_accounts, 0)?;
370 let freeze_escrow: Account<FreezeEscrow> = Account::try_from(freeze_pda)?;
371
372 let seeds = [
373 FreezeEscrow::PREFIX_SEED,
374 freeze_escrow.destination.as_ref(),
375 candy_guard_key.as_ref(),
376 candy_machine_key.as_ref(),
377 ];
378 let (pda, bump) = Pubkey::find_program_address(&seeds, &crate::ID);
379 assert_keys_equal(freeze_pda.key, &pda)?;
380
381 let authority = try_get_account_info(ctx.remaining_accounts, 1)?;
383
384 let authority_check = if let Some(candy_guard) = route_context.candy_guard {
387 candy_guard.authority
388 } else {
389 freeze_escrow.authority
390 };
391
392 if !(cmp_pubkeys(authority.key, &authority_check) && authority.is_signer) {
393 return err!(CandyGuardError::MissingRequiredSignature);
394 }
395
396 if freeze_escrow.frozen_count > 0 {
398 return err!(CandyGuardError::UnlockNotEnabled);
399 }
400
401 let freeze_ata = try_get_account_info(ctx.remaining_accounts, 2)?;
402 assert_owned_by(freeze_ata, &spl_token::ID)?;
403 let freeze_ata_account = TokenAccount::unpack(&freeze_ata.try_borrow_data()?)?;
404 assert_keys_equal(&freeze_ata_account.owner, freeze_pda.key)?;
405
406 let destination_ata_account = try_get_account_info(ctx.remaining_accounts, 3)?;
407 assert_keys_equal(&freeze_escrow.destination, destination_ata_account.key)?;
408
409 let token_program = try_get_account_info(ctx.remaining_accounts, 4)?;
410 assert_keys_equal(token_program.key, &Token::id())?;
411
412 let signer = [
415 FreezeEscrow::PREFIX_SEED,
416 freeze_escrow.destination.as_ref(),
417 candy_guard_key.as_ref(),
418 candy_machine_key.as_ref(),
419 &[bump],
420 ];
421
422 spl_token_transfer(TokenTransferParams {
423 source: freeze_ata.to_account_info(),
424 destination: destination_ata_account.to_account_info(),
425 authority: freeze_pda.to_account_info(),
426 authority_signer_seeds: &signer,
427 token_program: token_program.to_account_info(),
428 amount: freeze_ata_account.amount,
429 })?;
430
431 invoke_signed(
434 &close_account(
435 token_program.key,
436 freeze_ata.key,
437 authority.key,
438 freeze_pda.key,
439 &[],
440 )?,
441 &[
442 freeze_ata.to_account_info(),
443 authority.to_account_info(),
444 freeze_pda.to_account_info(),
445 token_program.to_account_info(),
446 ],
447 &[&signer],
448 )?;
449
450 freeze_escrow.close(authority.to_account_info())?;
452
453 Ok(())
454}