mpl_candy_guard/guards/
freeze_token_payment.rs

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/// Guard that charges an amount in a specified spl-token as payment for the mint with a freeze period.
27///
28/// List of accounts required:
29///
30///   0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow",
31///           destination_ata pubkey, candy guard pubkey, candy machine pubkey]`).
32///   1. `[]` Associate token account of the NFT (seeds `[payer pubkey, token
33///           program pubkey, nft mint pubkey]`).
34///   2. `[writable]` Token account holding the required amount.
35///   3. `[writable]` Associate token account of the Freeze PDA (seeds `[freeze PDA
36///                   pubkey, token program pubkey, nft mint pubkey]`).
37///   4. `[optional]` Authorization rule set for the minted pNFT.
38#[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    // amount
48        + 32 // token mint
49        + 32 // destination ata
50    }
51
52    fn mask() -> u64 {
53        GuardType::as_mask(GuardType::FreezeTokenPayment)
54    }
55
56    /// Instructions to interact with the freeze feature:
57    ///
58    ///  * initialize
59    ///  * thaw
60    ///  * unlock funds
61    fn instruction<'info>(
62        ctx: &Context<'_, '_, '_, 'info, Route<'info>>,
63        route_context: RouteContext<'info>,
64        data: Vec<u8>,
65    ) -> Result<()> {
66        // determines the instruction to execute
67        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            // List of accounts required:
76            //
77            //   0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow",
78            //                   destination_ata pubkey, candy guard pubkey, candy machine pubkey]`).
79            //   1. `[signer]` Candy Guard authority.
80            //   2. `[]` System program account.
81            //   3. `[writable]` Associate token account of the Freeze PDA (seeds `[freeze PDA
82            //                   pubkey, token program pubkey, nft mint pubkey]`).
83            //   4. `[]` Token mint account.
84            //   5. `[]` Token program account.
85            //   6. `[]` Associate token program account.
86            //   7. `[]` Address to receive the funds (must match the `destination_ata` address
87            //           of the guard configuration).
88            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                // initializes the freeze pda (the check of the authority as signer is done
106                // during the initialization)
107                initialize_freeze(ctx, route_context, data, destination)?;
108
109                // initializes the freeze ata
110
111                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                // spl token program
120                let token_program = try_get_account_info(ctx.remaining_accounts, 5)?;
121                assert_keys_equal(token_program.key, &spl_token::ID)?;
122                // spl associated token program
123                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            // Thaw an eligible NFT.
158            //
159            // List of accounts required:
160            //
161            //   0. `[writable]` Freeze PDA to receive the funds (seeds `["freeze_escrow",
162            //                   destination_ata pubkey, candy guard pubkey, candy machine pubkey]`).
163            //   1. `[]` Mint account for the NFT.
164            //   2. `[]` Address of the owner of the NFT.
165            //   3. `[writable]` Associate token account of the NFT.
166            //   4. `[]` Master Edition account of the NFT.
167            //   5. `[]` spl-token program ID.
168            //   6. `[]` Metaplex Token Metadata program.
169            //
170            // Remaining accounts required for Programmable NFTs:
171            //
172            //   7. `[writable]` Metadata account of the NFT.
173            //   8. `[writable]` Freeze PDA associated token account of the NFT.
174            //   9. `[]` System program.
175            //   10. `[]` Sysvar instructions account.
176            //   11. `[]` SPL Associated Token Account program.
177            //   12. `[optional, writable]` Owner token record account.
178            //   13. `[optional, writable]` Freeze PDA token record account.
179            //   14. `[optional]` Token Authorization Rules program.
180            //   15. `[optional]` Token Authorization Rules account.
181            FreezeInstruction::Thaw => {
182                msg!("Instruction: Thaw (FreezeTokenPayment guard)");
183                thaw_nft(ctx, route_context, data)
184            }
185            // Unlocks frozen funds.
186            //
187            // List of accounts required:
188            //
189            //   0. `[writable]` Freeze PDA (seeds `["freeze_escrow", destination_ata pubkey, candy guard pubkey,
190            //                   candy machine pubkey]`).
191            //   1. `[signer]` Candy Guard authority.
192            //   2. `[writable]` Associate token account of the Freeze PDA (seeds `[freeze PDA pubkey, token
193            //                   program pubkey, nft mint pubkey]`).
194            //   3. `[writable]` Address to receive the funds (must match the `destination_ata` address
195            //                   of the guard configuration).
196            //   4. `[]` Token program account.
197            //   5. `[]` System program account.
198            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        // validates the additional accounts
217
218        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            // for unitialized accounts, we need to check the derivation since the
241            // account will be created during mint only if it is an ATA
242
243            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            // validates if the existing account is a token account
255            assert_is_token_account(nft_ata, ctx.accounts.minter.key, ctx.accounts.nft_mint.key)?;
256        }
257
258        // it has to match the 'token' account (if present)
259        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        // validate freeze_pda ata
265        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        // the accounts have already been validated
316        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        // freezes the nft
352        freeze_nft(
353            ctx,
354            ctx.indices["freeze_token_payment"],
355            &self.destination_ata,
356            4,
357        )
358    }
359}
360
361// Helper function to unlocks frozen funds.
362fn 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    // authority must the a signer
382    let authority = try_get_account_info(ctx.remaining_accounts, 1)?;
383
384    // if the candy guard account is present, we check the authority against
385    // the candy guard authority; otherwise we use the freeze escrow authority
386    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    // all NFTs must be thaw
397    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    // transfer the tokens
413
414    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    // close the freeze ata
432
433    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    // the rent for the freeze escrow goes back to the authority
451    freeze_escrow.close(authority.to_account_info())?;
452
453    Ok(())
454}