From 0ef60653b504756a584cb56f5abd18550b73f36b Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:50:52 -0800 Subject: [PATCH] Revamp new collection permission check and check for permanent delegates (#2) * Revamp new collection permission check and check for permanent delegates * Add tests * Update comment * Update error code --- clients/js/src/generated/errors/mplCore.ts | 20 ++ clients/js/test/updateV2.test.ts | 189 ++++++++++++++++++ clients/rust/src/generated/errors/mpl_core.rs | 3 + idls/mpl_core.json | 5 + programs/mpl-core/src/error.rs | 4 + programs/mpl-core/src/plugins/mod.rs | 8 + programs/mpl-core/src/plugins/utils.rs | 6 +- programs/mpl-core/src/processor/update.rs | 31 ++- 8 files changed, 256 insertions(+), 10 deletions(-) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 0c7414a4..07f8a487 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -689,6 +689,26 @@ export class CannotAddDataSectionError extends ProgramError { codeToErrorMap.set(0x2f, CannotAddDataSectionError); nameToErrorMap.set('CannotAddDataSection', CannotAddDataSectionError); +/** PermanentDelegatesPreventMove: Cannot move asset to collection with permanent delegates */ +export class PermanentDelegatesPreventMoveError extends ProgramError { + override readonly name: string = 'PermanentDelegatesPreventMove'; + + readonly code: number = 0x30; // 48 + + constructor(program: Program, cause?: Error) { + super( + 'Cannot move asset to collection with permanent delegates', + program, + cause + ); + } +} +codeToErrorMap.set(0x30, PermanentDelegatesPreventMoveError); +nameToErrorMap.set( + 'PermanentDelegatesPreventMove', + PermanentDelegatesPreventMoveError +); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/test/updateV2.test.ts b/clients/js/test/updateV2.test.ts index c22a267c..bcee1b63 100644 --- a/clients/js/test/updateV2.test.ts +++ b/clients/js/test/updateV2.test.ts @@ -1215,3 +1215,192 @@ test('it cannot add asset to collection using additional update delegate on new await t.throwsAsync(result, { name: 'InvalidAuthority' }); }); + +test('it cannot add asset to collection if new collection contains permanent freeze delegate', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const collection = await createCollection(umi, { + plugins: [ + { + type: 'PermanentFreezeDelegate', + frozen: false, + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + permanentFreezeDelegate: { + authority: { + type: 'UpdateAuthority', + }, + frozen: false, + }, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'PermanentDelegatesPreventMove' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + permanentFreezeDelegate: { + authority: { + type: 'UpdateAuthority', + }, + frozen: false, + }, + }); +}); + +test('it cannot add asset to collection if new collection contains permanent transfer delegate', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const collection = await createCollection(umi, { + plugins: [ + { + type: 'PermanentTransferDelegate', + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + permanentTransferDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'PermanentDelegatesPreventMove' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + permanentTransferDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); +}); + +test('it cannot add asset to collection if new collection contains permanent burn delegate', async (t) => { + const umi = await createUmi(); + const asset = await createAsset(umi); + const collection = await createCollection(umi, { + plugins: [ + { + type: 'PermanentBurnDelegate', + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + permanentBurnDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); + + const result = update(umi, { + asset, + name: 'Test Bread 2', + uri: 'https://example.com/bread2', + newUpdateAuthority: updateAuthority('Collection', [collection.publicKey]), + newCollection: collection.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'PermanentDelegatesPreventMove' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + currentSize: 0, + numMinted: 0, + permanentBurnDelegate: { + authority: { + type: 'UpdateAuthority', + }, + }, + }); +}); diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 93e72b13..9b360311 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -154,6 +154,9 @@ pub enum MplCoreError { /// 47 (0x2F) - Cannot add a Data Section without a linked external plugin #[error("Cannot add a Data Section without a linked external plugin")] CannotAddDataSection, + /// 48 (0x30) - Cannot move asset to collection with permanent delegates + #[error("Cannot move asset to collection with permanent delegates")] + PermanentDelegatesPreventMove, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 4ea16d18..0a76392e 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -4852,6 +4852,11 @@ "code": 47, "name": "CannotAddDataSection", "msg": "Cannot add a Data Section without a linked external plugin" + }, + { + "code": 48, + "name": "PermanentDelegatesPreventMove", + "msg": "Cannot move asset to collection with permanent delegates" } ], "metadata": { diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index 266684c2..4756c95e 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -200,6 +200,10 @@ pub enum MplCoreError { /// 47 - Cannot add a Data Section without a linked external plugin #[error("Cannot add a Data Section without a linked external plugin")] CannotAddDataSection, + + /// 48 - Cannot move asset to collection with permanent delegates + #[error("Cannot move asset to collection with permanent delegates")] + PermanentDelegatesPreventMove, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/mod.rs b/programs/mpl-core/src/plugins/mod.rs index 29b4845d..f9c9e637 100644 --- a/programs/mpl-core/src/plugins/mod.rs +++ b/programs/mpl-core/src/plugins/mod.rs @@ -97,6 +97,7 @@ impl Compressible for Plugin {} BorshSerialize, BorshDeserialize, Eq, + Hash, PartialEq, ToPrimitive, EnumCount, @@ -136,6 +137,13 @@ pub enum PluginType { Autograph, } +/// The list of permanent delegate types. +pub const PERMANENT_DELEGATES: [PluginType; 3] = [ + PluginType::PermanentFreezeDelegate, + PluginType::PermanentTransferDelegate, + PluginType::PermanentBurnDelegate, +]; + impl DataBlob for PluginType { fn get_initial_size() -> usize { 2 diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 66d311d8..f2f25d1b 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -237,8 +237,10 @@ pub fn fetch_plugins(account: &AccountInfo) -> Result, Progr } /// List all plugins in an account. -pub fn list_plugins(account: &AccountInfo) -> Result, ProgramError> { - let asset = AssetV1::load(account, 0)?; +pub fn list_plugins( + account: &AccountInfo, +) -> Result, ProgramError> { + let asset = T::load(account, 0)?; if asset.get_size() == account.data_len() { return Err(MplCoreError::PluginNotFound.into()); diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index 23c6d9db..397a2aaa 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -3,6 +3,7 @@ use mpl_utils::assert_signer; use solana_program::{ account_info::AccountInfo, entrypoint::ProgramResult, msg, program_memory::sol_memcpy, }; +use std::collections::HashSet; use crate::{ error::MplCoreError, @@ -10,8 +11,8 @@ use crate::{ Context, UpdateCollectionV1Accounts, UpdateV1Accounts, UpdateV2Accounts, }, plugins::{ - fetch_plugin, ExternalPluginAdapter, HookableLifecycleEvent, Plugin, PluginHeaderV1, - PluginRegistryV1, PluginType, UpdateDelegate, + fetch_plugin, list_plugins, ExternalPluginAdapter, HookableLifecycleEvent, Plugin, + PluginHeaderV1, PluginRegistryV1, PluginType, UpdateDelegate, PERMANENT_DELEGATES, }, state::{AssetV1, CollectionV1, DataBlob, Key, SolanaAccount, UpdateAuthority}, utils::{ @@ -184,14 +185,28 @@ fn update<'a>( // Deserialize the collection. let mut new_collection = CollectionV1::load(new_collection_account, 0)?; - // See if there is an update delegate on the new collection. - let maybe_update_delegate = fetch_plugin::( - new_collection_account, - PluginType::UpdateDelegate, - ); + // Get a set of all the plugins on the collection (if any). + let plugin_set: HashSet<_> = + if new_collection_account.data_len() > new_collection.get_size() { + let plugin_list = list_plugins::(new_collection_account)?; + plugin_list.into_iter().collect() + } else { + HashSet::new() + }; + + // Cannot move to a collection with permanent delegates. + if PERMANENT_DELEGATES.iter().any(|p| plugin_set.contains(p)) { + return Err(MplCoreError::PermanentDelegatesPreventMove.into()); + } // Make sure the authority has authority to add the asset to the new collection. - if let Ok((plugin_authority, _, _)) = maybe_update_delegate { + if plugin_set.contains(&PluginType::UpdateDelegate) { + // Fetch the update delegate on the new collection. + let (plugin_authority, _, _) = fetch_plugin::( + new_collection_account, + PluginType::UpdateDelegate, + )?; + if assert_collection_authority( &new_collection, authority,