From 71a7f3b087914f4d81450c6411f6cd5a139c3b1f Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:51:32 -0800 Subject: [PATCH] Add update_metadata_collection_nft --- programs/bubblegum/program/src/error.rs | 4 + programs/bubblegum/program/src/lib.rs | 23 ++- .../program/src/processor/update_metadata.rs | 136 +++++++++++++++++- 3 files changed, 159 insertions(+), 4 deletions(-) diff --git a/programs/bubblegum/program/src/error.rs b/programs/bubblegum/program/src/error.rs index f5965226..011b1019 100644 --- a/programs/bubblegum/program/src/error.rs +++ b/programs/bubblegum/program/src/error.rs @@ -80,6 +80,10 @@ pub enum BubblegumError { MetadataArgsAmbiguous, #[msg("MetadataArgs missing")] MetadataArgsMissing, + #[msg("NFT linked to collection")] + NFTLinkedToCollection, + #[msg("NFT not linked to verified collection")] + NFTNotLinkedToVerifiedCollection, #[msg("Can only update primary sale to true")] PrimarySaleCanOnlyBeFlippedToTrue, #[msg("Creator did not unverify the metadata")] diff --git a/programs/bubblegum/program/src/lib.rs b/programs/bubblegum/program/src/lib.rs index 4e29eb23..24065d66 100644 --- a/programs/bubblegum/program/src/lib.rs +++ b/programs/bubblegum/program/src/lib.rs @@ -36,6 +36,7 @@ pub enum InstructionName { MintToCollectionV1, SetDecompressibleState, UpdateMetadata, + UpdateMetadataCollectionNft, } pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { @@ -64,6 +65,7 @@ pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { // `SetDecompressableState` instruction mapped to `SetDecompressibleState` instruction [18, 135, 238, 168, 246, 195, 61, 115] => InstructionName::SetDecompressibleState, [170, 182, 43, 239, 97, 78, 225, 186] => InstructionName::UpdateMetadata, + [244, 12, 175, 194, 227, 28, 102, 215] => InstructionName::UpdateMetadataCollectionNft, _ => InstructionName::Unknown, } } @@ -265,7 +267,7 @@ pub mod bubblegum { processor::verify_creator(ctx, root, data_hash, creator_hash, nonce, index, message) } - /// Verifies a creator for a leaf node. + /// Updates metadata for a leaf node that is not part of a verified collection. pub fn update_metadata<'info>( ctx: Context<'_, '_, '_, 'info, UpdateMetadata<'info>>, root: [u8; 32], @@ -276,4 +278,23 @@ pub mod bubblegum { ) -> Result<()> { processor::update_metadata(ctx, root, nonce, index, current_metadata, update_args) } + + /// Updates metadata for a leaf node that is part of a verified collection. + pub fn update_metadata_collection_nft<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateMetadataCollectionNFT<'info>>, + root: [u8; 32], + nonce: u64, + index: u32, + current_metadata: MetadataArgs, + update_args: UpdateArgs, + ) -> Result<()> { + processor::update_metadata_collection_nft( + ctx, + root, + nonce, + index, + current_metadata, + update_args, + ) + } } diff --git a/programs/bubblegum/program/src/processor/update_metadata.rs b/programs/bubblegum/program/src/processor/update_metadata.rs index e09ab61b..6aca8318 100644 --- a/programs/bubblegum/program/src/processor/update_metadata.rs +++ b/programs/bubblegum/program/src/processor/update_metadata.rs @@ -2,12 +2,12 @@ use anchor_lang::prelude::*; use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; use crate::{ - asserts::assert_metadata_is_mpl_compatible, + asserts::{assert_has_collection_authority, assert_metadata_is_mpl_compatible}, error::BubblegumError, state::{ leaf_schema::LeafSchema, - metaplex_adapter::{Creator, MetadataArgs, UpdateArgs}, - metaplex_anchor::MplTokenMetadata, + metaplex_adapter::{Collection, Creator, MetadataArgs, UpdateArgs}, + metaplex_anchor::{MplTokenMetadata, TokenMetadata}, TreeConfig, }, utils::{get_asset_id, hash_creators, hash_metadata, replace_leaf}, @@ -36,6 +36,80 @@ pub struct UpdateMetadata<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct UpdateMetadataCollectionNFT<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfig>, + pub tree_delegate: Signer<'info>, + pub collection_authority: Signer<'info>, + /// CHECK: This account is checked in the instruction + pub collection_mint: UncheckedAccount<'info>, + pub collection_metadata: Box>, + /// CHECK: This account is checked in the instruction + pub collection_authority_record_pda: Option>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + pub payer: Signer<'info>, + #[account(mut)] + /// CHECK: This account is modified in the downstream program + pub merkle_tree: UncheckedAccount<'info>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub token_metadata_program: Program<'info, MplTokenMetadata>, + pub system_program: Program<'info, System>, +} + +fn assert_authority_matches_collection<'info>( + collection: &Collection, + collection_authority: &AccountInfo<'info>, + collection_authority_record_pda: &Option>, + collection_mint: &AccountInfo<'info>, + collection_metadata_account_info: &AccountInfo, + collection_metadata: &TokenMetadata, + token_metadata_program: &Program<'info, MplTokenMetadata>, +) -> Result<()> { + // Mint account must match Collection mint + require!( + collection_mint.key() == collection.key, + BubblegumError::CollectionMismatch + ); + // Metadata mint must match Collection mint + require!( + collection_metadata.mint == collection.key, + BubblegumError::CollectionMismatch + ); + // Verify correct account ownerships. + require!( + *collection_metadata_account_info.owner == token_metadata_program.key(), + BubblegumError::IncorrectOwner + ); + // Collection mint must be owned by SPL token + require!( + *collection_mint.owner == spl_token::id(), + BubblegumError::IncorrectOwner + ); + + let collection_authority_record = collection_authority_record_pda + .as_ref() + .map(|authority_record_pda| authority_record_pda.to_account_info()); + + // Assert that the correct Collection Authority was provided using token-metadata + assert_has_collection_authority( + collection_metadata, + collection_mint.key, + collection_authority.key, + collection_authority_record.as_ref(), + )?; + + Ok(()) +} + fn all_verified_creators_in_a_are_in_b(a: &[Creator], b: &[Creator], exception: Pubkey) -> bool { a.iter() .filter(|creator_a| creator_a.verified) @@ -178,6 +252,62 @@ pub fn update_metadata<'info>( BubblegumError::TreeAuthorityIncorrect, ); + // NFTs which are linked to verified collections cannot be updated through this instruction + require!( + current_metadata.collection.is_none() + || !current_metadata.collection.as_ref().unwrap().verified, + BubblegumError::NFTLinkedToCollection + ); + + process_update_metadata( + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.tree_delegate, + &ctx.accounts.leaf_owner, + &ctx.accounts.leaf_delegate, + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.log_wrapper, + ctx.remaining_accounts, + root, + current_metadata, + update_args, + nonce, + index, + ) +} + +pub fn update_metadata_collection_nft<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateMetadataCollectionNFT<'info>>, + root: [u8; 32], + nonce: u64, + index: u32, + current_metadata: MetadataArgs, + update_args: UpdateArgs, +) -> Result<()> { + require!( + ctx.accounts.tree_delegate.key() == ctx.accounts.tree_authority.tree_creator + || ctx.accounts.tree_delegate.key() == ctx.accounts.tree_authority.tree_delegate, + BubblegumError::TreeAuthorityIncorrect, + ); + + // NFTs updated through this instruction must be linked to a collection, + // so a collection authority for that collection must sign + let collection = current_metadata + .collection + .as_ref() + .ok_or(BubblegumError::NFTNotLinkedToVerifiedCollection)?; + + assert_authority_matches_collection( + collection, + &ctx.accounts.collection_authority.to_account_info(), + &ctx.accounts.collection_authority_record_pda, + &ctx.accounts.collection_mint, + &ctx.accounts.collection_metadata.to_account_info(), + &ctx.accounts.collection_metadata, + &ctx.accounts.token_metadata_program, + )?; + process_update_metadata( &ctx.accounts.merkle_tree.to_account_info(), &ctx.accounts.tree_delegate,