mpl_token_metadata/utils/
metadata.rs

1use borsh::{maybestd::io::Error as BorshError, BorshDeserialize, BorshSerialize};
2use mpl_utils::{create_or_allocate_account_raw, token::get_mint_authority};
3use solana_program::{
4    account_info::AccountInfo, entrypoint::ProgramResult, program_option::COption, pubkey::Pubkey,
5};
6
7use super::{compression::is_decompression, *};
8use crate::{
9    assertions::{
10        assert_mint_authority_matches_mint, assert_owned_by,
11        collection::assert_collection_update_is_valid, metadata::assert_data_valid,
12        uses::assert_valid_use,
13    },
14    state::{
15        Collection, CollectionDetails, Data, DataV2, Key, Metadata, ProgrammableConfig,
16        TokenStandard, Uses, EDITION, MAX_METADATA_LEN, PREFIX,
17    },
18};
19
20// This equals the program address of the metadata program:
21//
22// AqH29mZfQFgRpfwaPoTMWSKJ5kqauoc1FwVBRksZyQrt
23//
24// IMPORTANT NOTE
25// This allows the upgrade authority of the Token Metadata program to create metadata for SPL tokens.
26// This only allows the upgrade authority to do create general metadata for the SPL token, it does not
27// allow the upgrade authority to add or change creators.
28pub const SEED_AUTHORITY: Pubkey = Pubkey::new_from_array([
29    0x92, 0x17, 0x2c, 0xc4, 0x72, 0x5d, 0xc0, 0x41, 0xf9, 0xdd, 0x8c, 0x51, 0x52, 0x60, 0x04, 0x26,
30    0x00, 0x93, 0xa3, 0x0b, 0x02, 0x73, 0xdc, 0xfa, 0x74, 0x92, 0x17, 0xfc, 0x94, 0xa2, 0x40, 0x49,
31]);
32
33// This allows the Bubblegum program to add verified creators since they were verified as part of
34// the Bubblegum program.
35
36pub struct CreateMetadataAccountsLogicArgs<'a> {
37    pub metadata_account_info: &'a AccountInfo<'a>,
38    pub mint_info: &'a AccountInfo<'a>,
39    pub mint_authority_info: &'a AccountInfo<'a>,
40    pub payer_account_info: &'a AccountInfo<'a>,
41    pub update_authority_info: &'a AccountInfo<'a>,
42    pub system_account_info: &'a AccountInfo<'a>,
43}
44
45/// Create a new account instruction
46pub fn process_create_metadata_accounts_logic(
47    program_id: &Pubkey,
48    accounts: CreateMetadataAccountsLogicArgs,
49    data: DataV2,
50    allow_direct_creator_writes: bool,
51    mut is_mutable: bool,
52    is_edition: bool,
53    add_token_standard: bool,
54    collection_details: Option<CollectionDetails>,
55) -> ProgramResult {
56    let CreateMetadataAccountsLogicArgs {
57        metadata_account_info,
58        mint_info,
59        mint_authority_info,
60        payer_account_info,
61        update_authority_info,
62        system_account_info,
63    } = accounts;
64
65    let mut update_authority_key = *update_authority_info.key;
66    let existing_mint_authority = get_mint_authority(mint_info)?;
67
68    // IMPORTANT NOTE:
69    // This allows the Metaplex Foundation to Create but not update metadata for SPL tokens that
70    // have not populated their metadata.
71    assert_mint_authority_matches_mint(&existing_mint_authority, mint_authority_info).or_else(
72        |e| {
73            // Allow seeding by the authority seed populator
74            if mint_authority_info.key == &SEED_AUTHORITY && mint_authority_info.is_signer {
75                // When metadata is seeded, the mint authority should be able to change it
76                if let COption::Some(auth) = existing_mint_authority {
77                    update_authority_key = auth;
78                    is_mutable = true;
79                }
80                Ok(())
81            } else {
82                Err(e)
83            }
84        },
85    )?;
86    assert_owned_by(mint_info, &spl_token::ID)?;
87
88    let metadata_seeds = &[
89        PREFIX.as_bytes(),
90        program_id.as_ref(),
91        mint_info.key.as_ref(),
92    ];
93    let (metadata_key, metadata_bump_seed) =
94        Pubkey::find_program_address(metadata_seeds, program_id);
95    let metadata_authority_signer_seeds = &[
96        PREFIX.as_bytes(),
97        program_id.as_ref(),
98        mint_info.key.as_ref(),
99        &[metadata_bump_seed],
100    ];
101
102    if metadata_account_info.key != &metadata_key {
103        return Err(MetadataError::InvalidMetadataKey.into());
104    }
105
106    create_or_allocate_account_raw(
107        *program_id,
108        metadata_account_info,
109        system_account_info,
110        payer_account_info,
111        MAX_METADATA_LEN,
112        metadata_authority_signer_seeds,
113    )?;
114
115    let mut metadata = Metadata::from_account_info(metadata_account_info)?;
116    let compatible_data = data.to_v1();
117
118    // This allows the Bubblegum program to create metadata with verified creators since they were
119    // verified already by the Bubblegum program.
120    let is_decompression = is_decompression(mint_info, mint_authority_info);
121    let allow_direct_creator_writes = allow_direct_creator_writes || is_decompression;
122
123    assert_data_valid(
124        &compatible_data,
125        &update_authority_key,
126        &metadata,
127        allow_direct_creator_writes,
128        update_authority_info.is_signer,
129    )?;
130
131    let mint_decimals = get_mint_decimals(mint_info)?;
132
133    metadata.mint = *mint_info.key;
134    metadata.key = Key::MetadataV1;
135    metadata.data = data.to_v1();
136    metadata.is_mutable = is_mutable;
137    metadata.update_authority = update_authority_key;
138
139    assert_valid_use(&data.uses, &None)?;
140    metadata.uses = data.uses;
141
142    // This allows for either print editions or the Bubblegum program to create metadata with verified collection.
143    let allow_direct_collection_verified_writes = is_edition || is_decompression;
144    assert_collection_update_is_valid(
145        allow_direct_collection_verified_writes,
146        &None,
147        &data.collection,
148    )?;
149    metadata.collection = data.collection;
150
151    // We want to create new collections with a size of zero but we use the
152    // collection details enum for forward compatibility.
153    if let Some(details) = collection_details {
154        match details {
155            CollectionDetails::V1 { size: _size } => {
156                metadata.collection_details = Some(CollectionDetails::V1 { size: 0 });
157            }
158        }
159    } else {
160        metadata.collection_details = None;
161    }
162
163    if add_token_standard {
164        let token_standard = if is_edition {
165            TokenStandard::NonFungibleEdition
166        } else if mint_decimals == 0 {
167            TokenStandard::FungibleAsset
168        } else {
169            TokenStandard::Fungible
170        };
171        metadata.token_standard = Some(token_standard);
172    } else {
173        metadata.token_standard = None;
174    }
175    puff_out_data_fields(&mut metadata);
176
177    let edition_seeds = &[
178        PREFIX.as_bytes(),
179        program_id.as_ref(),
180        metadata.mint.as_ref(),
181        EDITION.as_bytes(),
182    ];
183    let (_, edition_bump_seed) = Pubkey::find_program_address(edition_seeds, program_id);
184    metadata.edition_nonce = Some(edition_bump_seed);
185    // saves the changes to the account data
186    metadata.save(&mut metadata_account_info.data.borrow_mut())?;
187
188    Ok(())
189}
190
191// Custom deserialization function to handle NFTs with corrupted data.
192// This function is used in a custom deserialization implementation for the
193// `Metadata` struct, so should never have `msg` macros used in it as it may be used client side
194// either in tests or client code.
195//
196// It does not check `Key` type or account length and should only be used through the custom functions
197// `from_account_info` and `deserialize` implemented on the Metadata struct.
198pub fn meta_deser_unchecked(buf: &mut &[u8]) -> Result<Metadata, BorshError> {
199    // Metadata corruption shouldn't appear until after edition_nonce.
200    let key: Key = BorshDeserialize::deserialize(buf)?;
201    let update_authority: Pubkey = BorshDeserialize::deserialize(buf)?;
202    let mint: Pubkey = BorshDeserialize::deserialize(buf)?;
203    let data: Data = BorshDeserialize::deserialize(buf)?;
204    let primary_sale_happened: bool = BorshDeserialize::deserialize(buf)?;
205    let is_mutable: bool = BorshDeserialize::deserialize(buf)?;
206    let edition_nonce: Option<u8> = BorshDeserialize::deserialize(buf)?;
207
208    // V1.2
209    let token_standard_res: Result<Option<TokenStandard>, BorshError> =
210        BorshDeserialize::deserialize(buf);
211    let collection_res: Result<Option<Collection>, BorshError> = BorshDeserialize::deserialize(buf);
212    let uses_res: Result<Option<Uses>, BorshError> = BorshDeserialize::deserialize(buf);
213
214    // V1.3
215    let collection_details_res: Result<Option<CollectionDetails>, BorshError> =
216        BorshDeserialize::deserialize(buf);
217
218    // pNFT - Programmable Config
219    let programmable_config_res: Result<Option<ProgrammableConfig>, BorshError> =
220        BorshDeserialize::deserialize(buf);
221
222    // We can have accidentally valid, but corrupted data, particularly on the Collection struct,
223    // so to increase probability of catching errors. If any of these deserializations fail, set
224    // all values to None.
225    let (token_standard, collection, uses) = match (token_standard_res, collection_res, uses_res) {
226        (Ok(token_standard_res), Ok(collection_res), Ok(uses_res)) => {
227            (token_standard_res, collection_res, uses_res)
228        }
229        _ => (None, None, None),
230    };
231
232    // V1.3
233    let collection_details = match collection_details_res {
234        Ok(details) => details,
235        Err(_) => None,
236    };
237
238    // Programmable Config
239    let programmable_config = programmable_config_res.unwrap_or(None);
240
241    let metadata = Metadata {
242        key,
243        update_authority,
244        mint,
245        data,
246        primary_sale_happened,
247        is_mutable,
248        edition_nonce,
249        token_standard,
250        collection,
251        uses,
252        collection_details,
253        programmable_config,
254    };
255
256    Ok(metadata)
257}
258
259pub fn clean_write_metadata(
260    metadata: &mut Metadata,
261    metadata_account_info: &AccountInfo,
262) -> ProgramResult {
263    // Clear all data to ensure it is serialized cleanly with no trailing data due to creators array resizing.
264    let mut metadata_account_info_data = metadata_account_info.try_borrow_mut_data()?;
265    metadata_account_info_data[0..].fill(0);
266
267    metadata.serialize(&mut *metadata_account_info_data)?;
268
269    Ok(())
270}
271
272#[cfg(test)]
273pub mod tests {
274    use solana_program::pubkey;
275
276    use super::*;
277    pub use crate::{state::Creator, utils::puff_out_data_fields};
278
279    // Pesky Penguins #8060 (NOOT!)
280    // Corrupted data that can't be deserialized with the standard BoshDeserialization implementation.
281    pub fn pesky_data() -> &'static [u8] {
282        &[
283            4, 12, 25, 250, 103, 242, 3, 129, 143, 173, 110, 204, 157, 11, 1, 247, 211, 138, 199,
284            219, 79, 142, 183, 195, 96, 206, 63, 208, 102, 152, 127, 62, 43, 181, 253, 142, 126,
285            95, 96, 46, 202, 26, 76, 133, 228, 219, 191, 64, 186, 139, 115, 88, 216, 76, 125, 144,
286            12, 216, 198, 54, 196, 128, 102, 191, 96, 32, 0, 0, 0, 80, 101, 115, 107, 121, 32, 80,
287            101, 110, 103, 117, 105, 110, 115, 32, 35, 56, 48, 54, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0,
288            0, 0, 0, 10, 0, 0, 0, 78, 79, 79, 84, 0, 0, 0, 0, 0, 0, 200, 0, 0, 0, 104, 116, 116,
289            112, 115, 58, 47, 47, 97, 114, 119, 101, 97, 118, 101, 46, 110, 101, 116, 47, 72, 122,
290            79, 110, 102, 78, 77, 87, 81, 66, 72, 84, 57, 118, 48, 68, 87, 56, 69, 114, 57, 89, 70,
291            119, 100, 105, 71, 74, 88, 52, 45, 117, 75, 57, 82, 83, 89, 65, 82, 56, 102, 120, 69,
292            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
293            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
294            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
295            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
296            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 244, 1, 1, 3, 0, 0, 0,
297            135, 35, 134, 27, 83, 153, 173, 73, 166, 213, 73, 13, 254, 1, 156, 113, 34, 24, 205,
298            42, 233, 242, 137, 173, 173, 195, 214, 108, 110, 42, 89, 229, 1, 0, 12, 25, 250, 103,
299            242, 3, 129, 143, 173, 110, 204, 157, 11, 1, 247, 211, 138, 199, 219, 79, 142, 183,
300            195, 96, 206, 63, 208, 102, 152, 127, 62, 43, 1, 40, 12, 63, 245, 233, 144, 127, 205,
301            69, 77, 225, 56, 60, 107, 184, 84, 240, 194, 136, 55, 121, 217, 128, 246, 223, 140, 64,
302            40, 122, 145, 17, 203, 60, 0, 60, 1, 1, 1, 255, 149, 248, 123, 137, 230, 77, 203, 8,
303            124, 145, 63, 132, 220, 224, 64, 60, 253, 17, 33, 18, 81, 80, 186, 15, 248, 247, 249,
304            243, 1, 20, 26, 244, 47, 94, 35, 232, 64, 68, 124, 40, 100, 36, 93, 190, 82, 38, 36,
305            149, 248, 56, 72, 95, 68, 50, 157, 1, 155, 95, 113, 49, 247, 176, 1, 20, 1, 1, 1, 255,
306            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
307            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
308            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
309            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
310            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
311            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
312            0, 0, 0, 0, 0,
313        ]
314    }
315
316    pub fn expected_pesky_metadata() -> Metadata {
317        let creators = vec![
318            Creator {
319                address: pubkey!("A6XTVFiwGVsG6b6LsvQTGnV5LH3Pfa3qW3TGz8RjToLp"),
320                verified: true,
321                share: 0,
322            },
323            Creator {
324                address: pubkey!("pEsKYABNARLiDFYrjbjHDieD5h6gHrvYf9Vru62NX9k"),
325                verified: true,
326                share: 40,
327            },
328            Creator {
329                address: pubkey!("ppTeamTpw1cbC8ybJpppbnoL7xXD9froJNFb5uvoPvb"),
330                verified: false,
331                share: 60,
332            },
333        ];
334
335        let data = Data {
336            name: "Pesky Penguins #8060".to_string(),
337            symbol: "NOOT".to_string(),
338            uri: "https://arweave.net/HzOnfNMWQBHT9v0DW8Er9YFwdiGJX4-uK9RSYAR8fxE".to_string(),
339            seller_fee_basis_points: 500,
340            creators: Some(creators),
341        };
342
343        let mut metadata = Metadata {
344            key: Key::MetadataV1,
345            update_authority: pubkey!("pEsKYABNARLiDFYrjbjHDieD5h6gHrvYf9Vru62NX9k"),
346            mint: pubkey!("DFR3KjTso6PFCyUtq48a2aPZQpMMoaFgtbdxtaLxF2TR"),
347            data,
348            primary_sale_happened: true,
349            is_mutable: true,
350            edition_nonce: Some(255),
351            token_standard: None,
352            collection: None,
353            uses: None,
354            collection_details: None,
355            programmable_config: None,
356        };
357
358        puff_out_data_fields(&mut metadata);
359
360        metadata
361    }
362
363    #[test]
364    fn deserialize_corrupted_metadata() {
365        let mut buf = pesky_data();
366        let metadata = meta_deser_unchecked(&mut buf).unwrap();
367        let expected_metadata = expected_pesky_metadata();
368
369        assert_eq!(metadata, expected_metadata);
370    }
371}