Skip to content

Commit

Permalink
Revamp new collection permission check and check for permanent delega…
Browse files Browse the repository at this point in the history
…tes (#2) (#201)

* Revamp new collection permission check and check for permanent delegates

* Add tests

* Update comment

* Update error code
  • Loading branch information
danenbm authored Dec 6, 2024
1 parent 7d82981 commit 30a2dd2
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 10 deletions.
20 changes: 20 additions & 0 deletions clients/js/src/generated/errors/mplCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
189 changes: 189 additions & 0 deletions clients/js/test/updateV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
});
});
3 changes: 3 additions & 0 deletions clients/rust/src/generated/errors/mpl_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions idls/mpl_core.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions programs/mpl-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions programs/mpl-core/src/plugins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl Compressible for Plugin {}
BorshSerialize,
BorshDeserialize,
Eq,
Hash,
PartialEq,
ToPrimitive,
EnumCount,
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions programs/mpl-core/src/plugins/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,10 @@ pub fn fetch_plugins(account: &AccountInfo) -> Result<Vec<RegistryRecord>, Progr
}

/// List all plugins in an account.
pub fn list_plugins(account: &AccountInfo) -> Result<Vec<PluginType>, ProgramError> {
let asset = AssetV1::load(account, 0)?;
pub fn list_plugins<T: DataBlob + SolanaAccount>(
account: &AccountInfo,
) -> Result<Vec<PluginType>, ProgramError> {
let asset = T::load(account, 0)?;

if asset.get_size() == account.data_len() {
return Err(MplCoreError::PluginNotFound.into());
Expand Down
31 changes: 23 additions & 8 deletions programs/mpl-core/src/processor/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ 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,
instruction::accounts::{
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::{
Expand Down Expand Up @@ -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::<CollectionV1, UpdateDelegate>(
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::<CollectionV1>(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::<CollectionV1, UpdateDelegate>(
new_collection_account,
PluginType::UpdateDelegate,
)?;

if assert_collection_authority(
&new_collection,
authority,
Expand Down

0 comments on commit 30a2dd2

Please sign in to comment.