diff --git a/clients/js/src/generated/errors/mplBubblegum.ts b/clients/js/src/generated/errors/mplBubblegum.ts index 56691df0..8aac233c 100644 --- a/clients/js/src/generated/errors/mplBubblegum.ts +++ b/clients/js/src/generated/errors/mplBubblegum.ts @@ -578,6 +578,19 @@ export class CreatorDidNotUnverifyError extends ProgramError { codeToErrorMap.set(0x1797, CreatorDidNotUnverifyError); nameToErrorMap.set('CreatorDidNotUnverify', CreatorDidNotUnverifyError); +/** InvalidTokenStandard: Only NonFungible standard is supported */ +export class InvalidTokenStandardError extends ProgramError { + readonly name: string = 'InvalidTokenStandard'; + + readonly code: number = 0x1798; // 6040 + + constructor(program: Program, cause?: Error) { + super('Only NonFungible standard is supported', program, cause); + } +} +codeToErrorMap.set(0x1798, InvalidTokenStandardError); +nameToErrorMap.set('InvalidTokenStandard', InvalidTokenStandardError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/test/mintV1.test.ts b/clients/js/test/mintV1.test.ts index fe593930..7aa87127 100644 --- a/clients/js/test/mintV1.test.ts +++ b/clients/js/test/mintV1.test.ts @@ -3,9 +3,16 @@ import { generateSigner, none, publicKey, + some, } from '@metaplex-foundation/umi'; import test from 'ava'; -import { MetadataArgsArgs, fetchMerkleTree, hashLeaf, mintV1 } from '../src'; +import { + MetadataArgsArgs, + TokenStandard, + fetchMerkleTree, + hashLeaf, + mintV1, +} from '../src'; import { createTree, createUmi } from './_setup'; test('it can mint an NFT from a Bubblegum tree', async (t) => { @@ -46,3 +53,51 @@ test('it can mint an NFT from a Bubblegum tree', async (t) => { }); t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf)); }); + +test('it cannot mint an NFT from a Bubblegum tree because token standard is empty', async (t) => { + // Given an empty Bubblegum tree. + const umi = await createUmi(); + const merkleTree = await createTree(umi); + const leafOwner = generateSigner(umi).publicKey; + + // When we mint a new NFT from the tree using the following metadata. + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 500, // 5% + collection: none(), + creators: [], + tokenStandard: none(), + }; + const promise = mintV1(umi, { + leafOwner, + merkleTree, + metadata, + }).sendAndConfirm(umi); + // Then we expect a program error because metadata's token standard is empty. + await t.throwsAsync(promise, { name: 'InvalidTokenStandard' }); +}); + +test('it cannot mint an NFT from a Bubblegum tree because token standard is wrong', async (t) => { + // Given an empty Bubblegum tree. + const umi = await createUmi(); + const merkleTree = await createTree(umi); + const leafOwner = generateSigner(umi).publicKey; + + // When we mint a new NFT from the tree using the following metadata. + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 500, // 5% + collection: none(), + creators: [], + tokenStandard: some(TokenStandard.FungibleAsset), + }; + const promise = mintV1(umi, { + leafOwner, + merkleTree, + metadata, + }).sendAndConfirm(umi); + // Then we expect a program error because metadata's token standard is FungibleAsset which is wrong. + await t.throwsAsync(promise, { name: 'InvalidTokenStandard' }); +}); diff --git a/clients/rust/src/generated/errors/mpl_bubblegum.rs b/clients/rust/src/generated/errors/mpl_bubblegum.rs index 2be5943b..789e089a 100644 --- a/clients/rust/src/generated/errors/mpl_bubblegum.rs +++ b/clients/rust/src/generated/errors/mpl_bubblegum.rs @@ -130,6 +130,9 @@ pub enum MplBubblegumError { /// 6039 (0x1797) - Creator did not unverify the metadata #[error("Creator did not unverify the metadata")] CreatorDidNotUnverify, + /// 6040 (0x1798) - Only NonFungible standard is supported + #[error("Only NonFungible standard is supported")] + InvalidTokenStandard, } impl solana_program::program_error::PrintProgramError for MplBubblegumError { diff --git a/idls/bubblegum.json b/idls/bubblegum.json index 0f8b1d6c..ba94b021 100644 --- a/idls/bubblegum.json +++ b/idls/bubblegum.json @@ -2269,6 +2269,11 @@ "code": 6039, "name": "CreatorDidNotUnverify", "msg": "Creator did not unverify the metadata" + }, + { + "code": 6040, + "name": "InvalidTokenStandard", + "msg": "Only NonFungible standard is supported" } ], "metadata": { diff --git a/programs/bubblegum/program/src/asserts.rs b/programs/bubblegum/program/src/asserts.rs index cdb2b79b..8ca8f6f9 100644 --- a/programs/bubblegum/program/src/asserts.rs +++ b/programs/bubblegum/program/src/asserts.rs @@ -1,4 +1,8 @@ -use crate::{error::BubblegumError, state::metaplex_adapter::MetadataArgs, utils::cmp_pubkeys}; +use crate::{ + error::BubblegumError, + state::metaplex_adapter::{MetadataArgs, TokenStandard as MetadataTokenStandard}, + utils::cmp_pubkeys, +}; use anchor_lang::prelude::*; use mpl_token_metadata::{ accounts::{CollectionAuthorityRecord, Metadata, MetadataDelegateRecord}, @@ -187,3 +191,12 @@ pub fn assert_collection_membership( Ok(()) } + +/// Assert that the provided MetadataArgs contains info about Token Standard +/// and ensures that it's NonFungible +pub fn assert_metadata_token_standard(metadata: &MetadataArgs) -> Result<()> { + match metadata.token_standard { + Some(MetadataTokenStandard::NonFungible) => Ok(()), + _ => Err(BubblegumError::InvalidTokenStandard.into()), + } +} diff --git a/programs/bubblegum/program/src/error.rs b/programs/bubblegum/program/src/error.rs index 634fb060..7fbb54c7 100644 --- a/programs/bubblegum/program/src/error.rs +++ b/programs/bubblegum/program/src/error.rs @@ -84,6 +84,8 @@ pub enum BubblegumError { PrimarySaleCanOnlyBeFlippedToTrue, #[msg("Creator did not unverify the metadata")] CreatorDidNotUnverify, + #[msg("Only NonFungible standard is supported")] + InvalidTokenStandard, } // Converts certain Token Metadata errors into Bubblegum equivalents diff --git a/programs/bubblegum/program/src/processor/mint.rs b/programs/bubblegum/program/src/processor/mint.rs index 4020c089..3ec28885 100644 --- a/programs/bubblegum/program/src/processor/mint.rs +++ b/programs/bubblegum/program/src/processor/mint.rs @@ -1,11 +1,10 @@ -use std::collections::HashSet; - use anchor_lang::prelude::*; use solana_program::keccak; use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; +use std::collections::HashSet; use crate::{ - asserts::assert_metadata_is_mpl_compatible, + asserts::{assert_metadata_is_mpl_compatible, assert_metadata_token_standard}, error::BubblegumError, state::{leaf_schema::LeafSchema, metaplex_adapter::MetadataArgs, TreeConfig}, utils::{append_leaf, get_asset_id}, @@ -41,6 +40,7 @@ pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> let delegate = ctx.accounts.leaf_delegate.key(); let authority = &mut ctx.accounts.tree_authority; let merkle_tree = &ctx.accounts.merkle_tree; + if !authority.is_public { require!( incoming_tree_delegate == authority.tree_creator @@ -107,6 +107,8 @@ pub(crate) fn process_mint_v1<'info>( } } + assert_metadata_token_standard(&message)?; + // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace // instructions, without passing the entire, un-hashed MetadataArgs struct let metadata_args_hash = keccak::hashv(&[message.try_to_vec()?.as_slice()]); diff --git a/programs/bubblegum/program/src/processor/set_and_verify_collection.rs b/programs/bubblegum/program/src/processor/set_and_verify_collection.rs index b6bac586..07195923 100644 --- a/programs/bubblegum/program/src/processor/set_and_verify_collection.rs +++ b/programs/bubblegum/program/src/processor/set_and_verify_collection.rs @@ -1,8 +1,9 @@ use anchor_lang::prelude::*; use crate::{ - error::BubblegumError, processor::process_collection_verification, - processor::verify_collection::CollectionVerification, state::metaplex_adapter::MetadataArgs, + error::BubblegumError, + processor::{process_collection_verification, verify_collection::CollectionVerification}, + state::metaplex_adapter::MetadataArgs, }; pub(crate) fn set_and_verify_collection<'info>( diff --git a/programs/bubblegum/program/tests/simple.rs b/programs/bubblegum/program/tests/simple.rs index 37828fad..e8658c09 100644 --- a/programs/bubblegum/program/tests/simple.rs +++ b/programs/bubblegum/program/tests/simple.rs @@ -4,8 +4,10 @@ pub mod utils; use anchor_lang::solana_program::instruction::InstructionError; use solana_program_test::{tokio, BanksClientError}; -use solana_sdk::signature::{Keypair, Signer}; -use solana_sdk::transaction::TransactionError; +use solana_sdk::{ + signature::{Keypair, Signer}, + transaction::TransactionError, +}; use crate::utils::Error::BanksClient; use utils::{ diff --git a/programs/bubblegum/program/tests/utils/context.rs b/programs/bubblegum/program/tests/utils/context.rs index fd48c5d2..25adfb10 100644 --- a/programs/bubblegum/program/tests/utils/context.rs +++ b/programs/bubblegum/program/tests/utils/context.rs @@ -4,7 +4,9 @@ use super::{ clone_keypair, digital_asset::DigitalAsset, program_test, tree::Tree, BanksResult, DirtyClone, Error, LeafArgs, Result, }; -use bubblegum::state::metaplex_adapter::{Collection, Creator, MetadataArgs, TokenProgramVersion}; +use bubblegum::state::metaplex_adapter::{ + Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard as MetadataTokenStandard, +}; use mpl_token_metadata::{ accounts::CollectionAuthorityRecord, instructions::ApproveCollectionAuthorityBuilder, @@ -130,7 +132,7 @@ impl BubblegumTestContext { primary_sale_happened: false, is_mutable: false, edition_nonce: None, - token_standard: None, + token_standard: Some(MetadataTokenStandard::NonFungible), token_program_version: TokenProgramVersion::Original, collection: Some(Collection { verified: false,