apl_token_metadata/
processor.rs

1//! Program state processor
2
3use {
4    crate::{
5        error::MetadataError,
6        find_attributes_pda_with_program, find_metadata_pda_with_program,
7        instruction::MetadataInstruction,
8        state::{
9            TokenMetadata, TokenMetadataAttributes, DESCRIPTION_MAX_LEN, IMAGE_MAX_LEN,
10            MAX_ATTRIBUTES, MAX_KEY_LENGTH, MAX_VALUE_LENGTH, NAME_MAX_LEN, SYMBOL_MAX_LEN,
11        },
12        ATTRIBUTES_SEED, METADATA_SEED,
13    },
14    apl_token::{self, state::Mint},
15    arch_program::{
16        account::{next_account_info, AccountInfo},
17        entrypoint::ProgramResult,
18        msg,
19        program::invoke_signed,
20        program_error::ProgramError,
21        program_option::COption,
22        program_pack::{IsInitialized, Pack},
23        pubkey::Pubkey,
24        rent::minimum_rent,
25        system_instruction::create_account,
26    },
27};
28
29/// Program state handler.
30pub struct Processor {}
31
32impl Processor {
33    /// Process a single instruction
34    pub fn process(
35        program_id: &Pubkey,
36        accounts: &[AccountInfo],
37        instruction_data: &[u8],
38    ) -> ProgramResult {
39        let instruction = MetadataInstruction::unpack(instruction_data)?;
40
41        match instruction {
42            MetadataInstruction::CreateMetadata {
43                name,
44                symbol,
45                image,
46                description,
47                immutable,
48            } => Self::process_create_metadata(
49                program_id,
50                accounts,
51                name,
52                symbol,
53                image,
54                description,
55                immutable,
56            ),
57            MetadataInstruction::UpdateMetadata {
58                name,
59                symbol,
60                image,
61                description,
62            } => Self::process_update_metadata(accounts, name, symbol, image, description),
63            MetadataInstruction::CreateAttributes { data } => {
64                Self::process_create_attributes(program_id, accounts, data)
65            }
66            MetadataInstruction::ReplaceAttributes { data } => {
67                Self::process_replace_attributes(program_id, accounts, data)
68            }
69
70            MetadataInstruction::TransferAuthority { new_authority } => {
71                Self::process_transfer_authority(accounts, new_authority)
72            }
73
74            MetadataInstruction::MakeImmutable => Self::process_make_immutable(accounts),
75        }
76    }
77
78    fn process_create_metadata(
79        program_id: &Pubkey,
80        accounts: &[AccountInfo],
81        name: String,
82        symbol: String,
83        image: String,
84        description: String,
85        immutable: bool,
86    ) -> ProgramResult {
87        let account_info_iter = &mut accounts.iter();
88        let payer_info = next_account_info(account_info_iter)?; // [writable, signer]
89        let system_program_info = next_account_info(account_info_iter)?; // []
90        let mint_info = next_account_info(account_info_iter)?; // []
91        let metadata_info = next_account_info(account_info_iter)?; // [writable]
92        let mint_authority_info = next_account_info(account_info_iter)?; // [signer]
93
94        // Token mint must be owned by the token program
95        if mint_info.owner != &apl_token::id() {
96            msg!(
97                "Mint is not owned by the token program expected {:?}, got {:?}",
98                apl_token::id(),
99                mint_info.owner
100            );
101            return Err(ProgramError::IncorrectProgramId);
102        }
103
104        // Deserialize mint and enforce authority policy
105        let mint = Mint::unpack(&mint_info.data.borrow()) //
106            .map_err(|_| ProgramError::InvalidAccountData)?;
107
108        if !mint.is_initialized() {
109            msg!("Mint is not initialized");
110            return Err(ProgramError::UninitializedAccount);
111        }
112
113        // Check signer against mint authority, with freeze authority fallback
114        let matched_signer: Option<Pubkey> = match mint.mint_authority {
115            COption::Some(mint_auth) => {
116                if cmp_pubkeys(&mint_auth, mint_authority_info.key) {
117                    Some(mint_auth)
118                } else {
119                    msg!("Mint authority does not match expected authority");
120                    return Err(MetadataError::InvalidAuthority.into());
121                }
122            }
123            COption::None => match mint.freeze_authority {
124                COption::Some(freeze_auth) => {
125                    if cmp_pubkeys(&freeze_auth, mint_authority_info.key) {
126                        Some(freeze_auth)
127                    } else {
128                        msg!("Freeze authority does not match expected authority");
129                        return Err(MetadataError::InvalidAuthority.into());
130                    }
131                }
132                COption::None => {
133                    msg!("Mint has no mint or freeze authority");
134                    return Err(MetadataError::InvalidAuthority.into());
135                }
136            },
137        };
138
139        // Validate mint authority is signer
140        if !mint_authority_info.is_signer {
141            msg!("Mint authority is not a signer");
142            return Err(MetadataError::InvalidAuthority.into());
143        }
144
145        // Validate the metadata PDA address
146        let (expected_md_pda, md_bump) = find_metadata_pda_with_program(program_id, mint_info.key);
147        if !cmp_pubkeys(&expected_md_pda, metadata_info.key) {
148            msg!("Metadata PDA does not match expected PDA");
149            return Err(ProgramError::InvalidSeeds);
150        }
151
152        // Validate field sizes
153        if name.len() > NAME_MAX_LEN
154            || symbol.len() > SYMBOL_MAX_LEN
155            || image.len() > IMAGE_MAX_LEN
156            || description.len() > DESCRIPTION_MAX_LEN
157        {
158            msg!(
159                "Metadata field size is too long: name={}/{}, symbol={}/{}, image={}/{}, description={}/{}",
160                name.len(),
161                NAME_MAX_LEN,
162                symbol.len(),
163                SYMBOL_MAX_LEN,
164                image.len(),
165                IMAGE_MAX_LEN,
166                description.len(),
167                DESCRIPTION_MAX_LEN,
168            );
169            return Err(MetadataError::StringTooLong.into());
170        }
171
172        // If not owned by this program, create metadata PDA via CPI using PDA seeds
173        if metadata_info.owner != program_id {
174            // Require correct system program id
175            if *system_program_info.key != Pubkey::system_program() {
176                msg!("System program id does not match expected system program id");
177                return Err(ProgramError::IncorrectProgramId);
178            }
179
180            if !payer_info.is_signer {
181                msg!("Payer is not a signer");
182                return Err(ProgramError::MissingRequiredSignature);
183            }
184
185            let space = TokenMetadata::LEN as u64;
186            let lamports = minimum_rent(TokenMetadata::LEN);
187
188            invoke_signed(
189                &create_account(
190                    payer_info.key,
191                    metadata_info.key,
192                    lamports,
193                    space,
194                    program_id,
195                ),
196                &[
197                    payer_info.clone(),
198                    metadata_info.clone(),
199                    system_program_info.clone(),
200                ],
201                &[&[
202                    METADATA_SEED, //
203                    mint_info.key.as_ref(),
204                    &[md_bump],
205                ]],
206            )?;
207        }
208
209        // Ensure not already initialized (zero-initialized account will have first byte == 0)
210        {
211            let data_ref = metadata_info.data.borrow();
212            if !data_ref.is_empty() && data_ref[0] != 0 {
213                return Err(MetadataError::MetadataAlreadyExists.into());
214            }
215        }
216
217        // Write metadata
218        let metadata = TokenMetadata {
219            is_initialized: true,
220            mint: *mint_info.key,
221            name,
222            symbol,
223            image,
224            description,
225            update_authority: if immutable { None } else { matched_signer },
226        };
227
228        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
229
230        Ok(())
231    }
232
233    fn process_update_metadata(
234        accounts: &[AccountInfo],
235        name: Option<String>,
236        symbol: Option<String>,
237        image: Option<String>,
238        description: Option<String>,
239    ) -> ProgramResult {
240        let account_info_iter = &mut accounts.iter();
241        let metadata_info = next_account_info(account_info_iter)?; // [writable]
242        let update_authority_info = next_account_info(account_info_iter)?; // [signer]
243
244        if !update_authority_info.is_signer {
245            msg!("Update authority is not a signer");
246            return Err(ProgramError::MissingRequiredSignature);
247        }
248
249        // Load existing metadata
250        let mut metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
251            .map_err(|_| ProgramError::InvalidAccountData)?;
252
253        if !metadata.is_initialized() {
254            msg!("Metadata not initialized");
255            return Err(ProgramError::UninitializedAccount);
256        }
257
258        // Enforce update authority (immutable if None)
259        match metadata.update_authority {
260            Some(current_auth) => {
261                if !cmp_pubkeys(&current_auth, update_authority_info.key) {
262                    msg!("Update authority does not match");
263                    return Err(MetadataError::InvalidAuthority.into());
264                }
265            }
266            None => {
267                msg!("Metadata is immutable");
268                return Err(MetadataError::InvalidAuthority.into());
269            }
270        }
271
272        // Validate and apply optional fields
273        if let Some(ref n) = name {
274            if n.len() > NAME_MAX_LEN {
275                return Err(MetadataError::StringTooLong.into());
276            }
277        }
278        if let Some(ref s) = symbol {
279            if s.len() > SYMBOL_MAX_LEN {
280                return Err(MetadataError::StringTooLong.into());
281            }
282        }
283        if let Some(ref i) = image {
284            if i.len() > IMAGE_MAX_LEN {
285                return Err(MetadataError::StringTooLong.into());
286            }
287        }
288        if let Some(ref d) = description {
289            if d.len() > DESCRIPTION_MAX_LEN {
290                return Err(MetadataError::StringTooLong.into());
291            }
292        }
293
294        if let Some(n) = name {
295            metadata.name = n;
296        }
297        if let Some(s) = symbol {
298            metadata.symbol = s;
299        }
300        if let Some(i) = image {
301            metadata.image = i;
302        }
303        if let Some(d) = description {
304            metadata.description = d;
305        }
306
307        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
308        Ok(())
309    }
310
311    fn process_create_attributes(
312        program_id: &Pubkey,
313        accounts: &[AccountInfo],
314        data: Vec<(String, String)>,
315    ) -> ProgramResult {
316        let account_info_iter = &mut accounts.iter();
317        let payer_info = next_account_info(account_info_iter)?; // [writable, signer]
318        let system_program_info = next_account_info(account_info_iter)?; // []
319        let mint_info = next_account_info(account_info_iter)?; // []
320        let attributes_info = next_account_info(account_info_iter)?; // [writable]
321        let update_authority_info = next_account_info(account_info_iter)?; // [signer]
322        let metadata_info = next_account_info(account_info_iter)?; // [] (readonly)
323
324        if !payer_info.is_signer || !update_authority_info.is_signer {
325            return Err(ProgramError::MissingRequiredSignature);
326        }
327
328        // Validate attribute PDA address using this program_id
329        let (expected_attrs_pda, attrs_bump) =
330            find_attributes_pda_with_program(program_id, mint_info.key);
331        if !cmp_pubkeys(&expected_attrs_pda, attributes_info.key) {
332            msg!("Attributes PDA does not match expected PDA");
333            return Err(ProgramError::InvalidSeeds);
334        }
335
336        // Ensure metadata exists and authority matches
337        let metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
338            .map_err(|_| ProgramError::InvalidAccountData)?;
339        if !metadata.is_initialized() || !cmp_pubkeys(&metadata.mint, mint_info.key) {
340            msg!("Metadata not initialized or mint mismatch");
341            return Err(ProgramError::InvalidAccountData);
342        }
343
344        match metadata.update_authority {
345            Some(current_auth) => {
346                if !cmp_pubkeys(&current_auth, update_authority_info.key) {
347                    msg!("Update authority does not match");
348                    return Err(MetadataError::InvalidAuthority.into());
349                }
350            }
351            None => {
352                msg!("Metadata is immutable");
353                return Err(MetadataError::InvalidAuthority.into());
354            }
355        }
356
357        // Validate vector sizes and elements
358        if data.len() > MAX_ATTRIBUTES {
359            msg!("Too many attributes: {} > {}", data.len(), MAX_ATTRIBUTES);
360            return Err(MetadataError::TooManyAttributes.into());
361        }
362        for (k, v) in &data {
363            if k.is_empty() || v.is_empty() {
364                msg!("Attribute key and value must be non-empty");
365                return Err(MetadataError::InvalidInstructionData.into());
366            }
367            if k.len() > MAX_KEY_LENGTH || v.len() > MAX_VALUE_LENGTH {
368                msg!(
369                    "Attribute key or value is too long: key={}/{}, value={}/{}",
370                    k.len(),
371                    MAX_KEY_LENGTH,
372                    v.len(),
373                    MAX_VALUE_LENGTH,
374                );
375                return Err(MetadataError::StringTooLong.into());
376            }
377        }
378
379        // Allocate full max size so future replacements never need reallocation
380        let required_space: u64 = TokenMetadataAttributes::LEN as u64;
381
382        if attributes_info.owner != program_id {
383            if *system_program_info.key != Pubkey::system_program() {
384                msg!("System program id does not match expected system program id");
385                return Err(ProgramError::IncorrectProgramId);
386            }
387            if !payer_info.is_signer {
388                msg!("Payer is not a signer");
389                return Err(ProgramError::MissingRequiredSignature);
390            }
391            let lamports = minimum_rent(TokenMetadataAttributes::LEN);
392
393            invoke_signed(
394                &create_account(
395                    payer_info.key,
396                    attributes_info.key,
397                    lamports,
398                    required_space,
399                    program_id,
400                ),
401                &[
402                    payer_info.clone(),
403                    attributes_info.clone(),
404                    system_program_info.clone(),
405                ],
406                &[&[
407                    ATTRIBUTES_SEED, //
408                    mint_info.key.as_ref(),
409                    &[attrs_bump],
410                ]],
411            )?;
412        } else {
413            let curr_len = attributes_info.data.borrow().len() as u64;
414            if curr_len != required_space {
415                msg!(
416                    "Attributes account size mismatch: curr={} required={}",
417                    curr_len,
418                    required_space
419                );
420                return Err(ProgramError::InvalidAccountData);
421            }
422        }
423
424        // Ensure not already initialized
425        {
426            let data_ref = attributes_info.data.borrow();
427            if !data_ref.is_empty() && data_ref[0] != 0 {
428                return Err(MetadataError::MetadataAlreadyExists.into());
429            }
430        }
431
432        let attrs = TokenMetadataAttributes {
433            is_initialized: true,
434            mint: *mint_info.key,
435            data,
436        };
437        attrs.pack_into_slice(&mut attributes_info.data.borrow_mut());
438        Ok(())
439    }
440
441    fn process_replace_attributes(
442        program_id: &Pubkey,
443        accounts: &[AccountInfo],
444        data: Vec<(String, String)>,
445    ) -> ProgramResult {
446        let account_info_iter = &mut accounts.iter();
447        let attributes_info = next_account_info(account_info_iter)?; // [writable]
448        let update_authority_info = next_account_info(account_info_iter)?; // [signer]
449        let metadata_info = next_account_info(account_info_iter)?; // [] (readonly)
450
451        if !update_authority_info.is_signer {
452            return Err(ProgramError::MissingRequiredSignature);
453        }
454
455        // Validate metadata and authority
456        let metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
457            .map_err(|_| ProgramError::InvalidAccountData)?;
458        if !metadata.is_initialized() {
459            return Err(ProgramError::UninitializedAccount);
460        }
461        match metadata.update_authority {
462            Some(current_auth) => {
463                if !cmp_pubkeys(&current_auth, update_authority_info.key) {
464                    return Err(MetadataError::InvalidAuthority.into());
465                }
466            }
467            None => return Err(MetadataError::InvalidAuthority.into()),
468        }
469
470        // Validate PDA
471        let (expected_attrs_pda, _bump) =
472            find_attributes_pda_with_program(program_id, &metadata.mint);
473        if !cmp_pubkeys(&expected_attrs_pda, attributes_info.key) {
474            return Err(ProgramError::InvalidSeeds);
475        }
476
477        // Ensure attributes exist
478        let mut attrs = TokenMetadataAttributes::unpack(&attributes_info.data.borrow())
479            .map_err(|_| ProgramError::InvalidAccountData)?;
480        if !attrs.is_initialized() {
481            return Err(ProgramError::UninitializedAccount);
482        }
483
484        // Validate sizes
485        if data.len() > MAX_ATTRIBUTES {
486            return Err(MetadataError::TooManyAttributes.into());
487        }
488        for (k, v) in &data {
489            if k.is_empty() || v.is_empty() {
490                return Err(MetadataError::InvalidInstructionData.into());
491            }
492            if k.len() > MAX_KEY_LENGTH || v.len() > MAX_VALUE_LENGTH {
493                return Err(MetadataError::StringTooLong.into());
494            }
495        }
496
497        // Replace vector
498        attrs.data = data;
499        attrs.pack_into_slice(&mut attributes_info.data.borrow_mut());
500        Ok(())
501    }
502
503    fn process_transfer_authority(
504        accounts: &[AccountInfo],
505        new_authority: Pubkey,
506    ) -> ProgramResult {
507        let account_info_iter = &mut accounts.iter();
508        let metadata_info = next_account_info(account_info_iter)?; // [writable]
509        let current_authority_info = next_account_info(account_info_iter)?; // [signer]
510
511        if !current_authority_info.is_signer {
512            msg!("Current authority is not a signer");
513            return Err(ProgramError::MissingRequiredSignature);
514        }
515
516        let mut metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
517            .map_err(|_| ProgramError::InvalidAccountData)?;
518        if !metadata.is_initialized() {
519            msg!("Metadata not initialized");
520            return Err(ProgramError::UninitializedAccount);
521        }
522
523        match metadata.update_authority {
524            Some(current_auth) => {
525                if !cmp_pubkeys(&current_auth, current_authority_info.key) {
526                    msg!("Signer is not current update authority");
527                    return Err(MetadataError::InvalidAuthority.into());
528                }
529            }
530            None => {
531                msg!("Metadata is immutable; cannot transfer authority");
532                return Err(MetadataError::InvalidAuthority.into());
533            }
534        }
535
536        metadata.update_authority = Some(new_authority);
537        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
538        Ok(())
539    }
540
541    fn process_make_immutable(accounts: &[AccountInfo]) -> ProgramResult {
542        let account_info_iter = &mut accounts.iter();
543        let metadata_info = next_account_info(account_info_iter)?; // [writable]
544        let current_authority_info = next_account_info(account_info_iter)?; // [signer]
545
546        if !current_authority_info.is_signer {
547            msg!("Current authority is not a signer");
548            return Err(ProgramError::MissingRequiredSignature);
549        }
550
551        let mut metadata = TokenMetadata::unpack(&metadata_info.data.borrow())
552            .map_err(|_| ProgramError::InvalidAccountData)?;
553        if !metadata.is_initialized() {
554            msg!("Metadata not initialized");
555            return Err(ProgramError::UninitializedAccount);
556        }
557
558        match metadata.update_authority {
559            Some(current_auth) => {
560                if !cmp_pubkeys(&current_auth, current_authority_info.key) {
561                    msg!("Signer is not current update authority");
562                    return Err(MetadataError::InvalidAuthority.into());
563                }
564            }
565            None => {
566                msg!("Metadata already immutable");
567                return Err(MetadataError::InvalidAuthority.into());
568            }
569        }
570
571        metadata.update_authority = None;
572        metadata.pack_into_slice(&mut metadata_info.data.borrow_mut());
573        Ok(())
574    }
575}
576
577/// Checks two pubkeys for equality using a cheap memcmp
578fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool {
579    arch_program::program_memory::sol_memcmp(a.as_ref(), b.as_ref(), 32) == 0
580}