Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp new collection permission check #201

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading