diff --git a/clients/js/src/generated/instructions/update.ts b/clients/js/src/generated/instructions/update.ts index 198f7774..82101b62 100644 --- a/clients/js/src/generated/instructions/update.ts +++ b/clients/js/src/generated/instructions/update.ts @@ -34,6 +34,8 @@ import { export type UpdateInstructionAccounts = { /** The address of the asset */ asset: PublicKey | Pda; + /** The collection to which the asset belongs */ + collection?: PublicKey | Pda; /** The update authority or update authority delegate of the asset */ authority?: Signer; /** The account paying for the storage fees */ @@ -96,28 +98,33 @@ export function update( isWritable: true as boolean, value: input.asset ?? null, }, - authority: { + collection: { index: 1, isWritable: false as boolean, + value: input.collection ?? null, + }, + authority: { + index: 2, + isWritable: false as boolean, value: input.authority ?? null, }, payer: { - index: 2, + index: 3, isWritable: true as boolean, value: input.payer ?? null, }, newUpdateAuthority: { - index: 3, + index: 4, isWritable: false as boolean, value: input.newUpdateAuthority ?? null, }, systemProgram: { - index: 4, + index: 5, isWritable: false as boolean, value: input.systemProgram ?? null, }, logWrapper: { - index: 5, + index: 6, isWritable: false as boolean, value: input.logWrapper ?? null, }, diff --git a/clients/js/test/plugins/asset/burn.test.ts b/clients/js/test/burn.test.ts similarity index 65% rename from clients/js/test/plugins/asset/burn.test.ts rename to clients/js/test/burn.test.ts index b2d66b04..9301a9b4 100644 --- a/clients/js/test/plugins/asset/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -13,8 +13,9 @@ import { burn, Key, updateAuthority, -} from '../../../src'; -import { createUmi } from '../../_setup'; + plugin, +} from '../src'; +import { createUmi } from './_setup'; test('it can burn an asset as the owner', async (t) => { // Given a Umi instance and a new signer. @@ -99,3 +100,50 @@ test('it cannot burn an asset if not the owner', async (t) => { uri: 'https://example.com/bread', }); }); + +test('it cannot burn an asset if it is frozen', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const assetAddress = generateSigner(umi); + const attacker = generateSigner(umi); + + // When we create a new account. + await create(umi, { + dataState: DataState.AccountState, + asset: assetAddress, + name: 'Test Bread', + uri: 'https://example.com/bread', + plugins: [ + plugin('Freeze', [{ frozen: true }]), + ], + }).sendAndConfirm(umi); + + // Then an account was created with the correct data. + const beforeAsset = await fetchAsset(umi, assetAddress.publicKey); + // console.log("Account State:", beforeAsset); + t.like(beforeAsset, { + publicKey: assetAddress.publicKey, + updateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + owner: umi.identity.publicKey, + name: 'Test Bread', + uri: 'https://example.com/bread', + }); + + const result = burn(umi, { + asset: assetAddress.publicKey, + compressionProof: null, + authority: attacker, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + const afterAsset = await fetchAsset(umi, assetAddress.publicKey); + // console.log("Account State:", afterAsset); + t.like(afterAsset, { + publicKey: assetAddress.publicKey, + updateAuthority: updateAuthority('Address', [umi.identity.publicKey]), + owner: umi.identity.publicKey, + name: 'Test Bread', + uri: 'https://example.com/bread', + }); +}); diff --git a/clients/rust/src/generated/instructions/update.rs b/clients/rust/src/generated/instructions/update.rs index f100e36a..96992d77 100644 --- a/clients/rust/src/generated/instructions/update.rs +++ b/clients/rust/src/generated/instructions/update.rs @@ -12,6 +12,8 @@ use borsh::BorshSerialize; pub struct Update { /// The address of the asset pub asset: solana_program::pubkey::Pubkey, + /// The collection to which the asset belongs + pub collection: Option, /// The update authority or update authority delegate of the asset pub authority: solana_program::pubkey::Pubkey, /// The account paying for the storage fees @@ -37,10 +39,20 @@ impl Update { args: UpdateInstructionArgs, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { - let mut accounts = Vec::with_capacity(6 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( self.asset, false, )); + if let Some(collection) = self.collection { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + collection, false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } accounts.push(solana_program::instruction::AccountMeta::new_readonly( self.authority, true, @@ -115,14 +127,16 @@ pub struct UpdateInstructionArgs { /// ### Accounts: /// /// 0. `[writable]` asset -/// 1. `[signer]` authority -/// 2. `[writable, signer, optional]` payer -/// 3. `[optional]` new_update_authority -/// 4. `[optional]` system_program (default to `11111111111111111111111111111111`) -/// 5. `[optional]` log_wrapper +/// 1. `[optional]` collection +/// 2. `[signer]` authority +/// 3. `[writable, signer, optional]` payer +/// 4. `[optional]` new_update_authority +/// 5. `[optional]` system_program (default to `11111111111111111111111111111111`) +/// 6. `[optional]` log_wrapper #[derive(Default)] pub struct UpdateBuilder { asset: Option, + collection: Option, authority: Option, payer: Option, new_update_authority: Option, @@ -143,6 +157,13 @@ impl UpdateBuilder { self.asset = Some(asset); self } + /// `[optional account]` + /// The collection to which the asset belongs + #[inline(always)] + pub fn collection(&mut self, collection: Option) -> &mut Self { + self.collection = collection; + self + } /// The update authority or update authority delegate of the asset #[inline(always)] pub fn authority(&mut self, authority: solana_program::pubkey::Pubkey) -> &mut Self { @@ -217,6 +238,7 @@ impl UpdateBuilder { pub fn instruction(&self) -> solana_program::instruction::Instruction { let accounts = Update { asset: self.asset.expect("asset is not set"), + collection: self.collection, authority: self.authority.expect("authority is not set"), payer: self.payer, new_update_authority: self.new_update_authority, @@ -238,6 +260,8 @@ impl UpdateBuilder { pub struct UpdateCpiAccounts<'a, 'b> { /// The address of the asset pub asset: &'b solana_program::account_info::AccountInfo<'a>, + /// The collection to which the asset belongs + pub collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The update authority or update authority delegate of the asset pub authority: &'b solana_program::account_info::AccountInfo<'a>, /// The account paying for the storage fees @@ -256,6 +280,8 @@ pub struct UpdateCpi<'a, 'b> { pub __program: &'b solana_program::account_info::AccountInfo<'a>, /// The address of the asset pub asset: &'b solana_program::account_info::AccountInfo<'a>, + /// The collection to which the asset belongs + pub collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, /// The update authority or update authority delegate of the asset pub authority: &'b solana_program::account_info::AccountInfo<'a>, /// The account paying for the storage fees @@ -279,6 +305,7 @@ impl<'a, 'b> UpdateCpi<'a, 'b> { Self { __program: program, asset: accounts.asset, + collection: accounts.collection, authority: accounts.authority, payer: accounts.payer, new_update_authority: accounts.new_update_authority, @@ -320,11 +347,22 @@ impl<'a, 'b> UpdateCpi<'a, 'b> { bool, )], ) -> solana_program::entrypoint::ProgramResult { - let mut accounts = Vec::with_capacity(6 + remaining_accounts.len()); + let mut accounts = Vec::with_capacity(7 + remaining_accounts.len()); accounts.push(solana_program::instruction::AccountMeta::new( *self.asset.key, false, )); + if let Some(collection) = self.collection { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + *collection.key, + false, + )); + } else { + accounts.push(solana_program::instruction::AccountMeta::new_readonly( + crate::MPL_CORE_ID, + false, + )); + } accounts.push(solana_program::instruction::AccountMeta::new_readonly( *self.authority.key, true, @@ -381,9 +419,12 @@ impl<'a, 'b> UpdateCpi<'a, 'b> { accounts, data, }; - let mut account_infos = Vec::with_capacity(6 + 1 + remaining_accounts.len()); + let mut account_infos = Vec::with_capacity(7 + 1 + remaining_accounts.len()); account_infos.push(self.__program.clone()); account_infos.push(self.asset.clone()); + if let Some(collection) = self.collection { + account_infos.push(collection.clone()); + } account_infos.push(self.authority.clone()); if let Some(payer) = self.payer { account_infos.push(payer.clone()); @@ -412,11 +453,12 @@ impl<'a, 'b> UpdateCpi<'a, 'b> { /// ### Accounts: /// /// 0. `[writable]` asset -/// 1. `[signer]` authority -/// 2. `[writable, signer, optional]` payer -/// 3. `[optional]` new_update_authority -/// 4. `[]` system_program -/// 5. `[optional]` log_wrapper +/// 1. `[optional]` collection +/// 2. `[signer]` authority +/// 3. `[writable, signer, optional]` payer +/// 4. `[optional]` new_update_authority +/// 5. `[]` system_program +/// 6. `[optional]` log_wrapper pub struct UpdateCpiBuilder<'a, 'b> { instruction: Box>, } @@ -426,6 +468,7 @@ impl<'a, 'b> UpdateCpiBuilder<'a, 'b> { let instruction = Box::new(UpdateCpiBuilderInstruction { __program: program, asset: None, + collection: None, authority: None, payer: None, new_update_authority: None, @@ -443,6 +486,16 @@ impl<'a, 'b> UpdateCpiBuilder<'a, 'b> { self.instruction.asset = Some(asset); self } + /// `[optional account]` + /// The collection to which the asset belongs + #[inline(always)] + pub fn collection( + &mut self, + collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, + ) -> &mut Self { + self.instruction.collection = collection; + self + } /// The update authority or update authority delegate of the asset #[inline(always)] pub fn authority( @@ -553,6 +606,8 @@ impl<'a, 'b> UpdateCpiBuilder<'a, 'b> { asset: self.instruction.asset.expect("asset is not set"), + collection: self.instruction.collection, + authority: self.instruction.authority.expect("authority is not set"), payer: self.instruction.payer, @@ -577,6 +632,7 @@ impl<'a, 'b> UpdateCpiBuilder<'a, 'b> { struct UpdateCpiBuilderInstruction<'a, 'b> { __program: &'b solana_program::account_info::AccountInfo<'a>, asset: Option<&'b solana_program::account_info::AccountInfo<'a>>, + collection: Option<&'b solana_program::account_info::AccountInfo<'a>>, authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, payer: Option<&'b solana_program::account_info::AccountInfo<'a>>, new_update_authority: Option<&'b solana_program::account_info::AccountInfo<'a>>, diff --git a/idls/mpl_core.json b/idls/mpl_core.json index f035181d..449c422a 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -972,6 +972,15 @@ "The address of the asset" ] }, + { + "name": "collection", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "The collection to which the asset belongs" + ] + }, { "name": "authority", "isMut": false, diff --git a/programs/mpl-core/src/instruction.rs b/programs/mpl-core/src/instruction.rs index 970fc335..2c4bd1ef 100644 --- a/programs/mpl-core/src/instruction.rs +++ b/programs/mpl-core/src/instruction.rs @@ -147,11 +147,12 @@ pub(crate) enum MplAssetInstruction { /// Update an mpl-core. #[account(0, writable, name="asset", desc = "The address of the asset")] - #[account(1, signer, name="authority", desc = "The update authority or update authority delegate of the asset")] - #[account(2, optional, writable, signer, name="payer", desc = "The account paying for the storage fees")] - #[account(3, optional, name="new_update_authority", desc = "The new update authority of the asset")] - #[account(4, name="system_program", desc = "The system program")] - #[account(5, optional, name="log_wrapper", desc = "The SPL Noop Program")] + #[account(1, optional, name="collection", desc = "The collection to which the asset belongs")] + #[account(2, signer, name="authority", desc = "The update authority or update authority delegate of the asset")] + #[account(3, optional, writable, signer, name="payer", desc = "The account paying for the storage fees")] + #[account(4, optional, name="new_update_authority", desc = "The new update authority of the asset")] + #[account(5, name="system_program", desc = "The system program")] + #[account(6, optional, name="log_wrapper", desc = "The SPL Noop Program")] Update(UpdateArgs), /// Update an mpl-core. diff --git a/programs/mpl-core/src/plugins/burn.rs b/programs/mpl-core/src/plugins/burn.rs index 24b9ed7f..7d83a190 100644 --- a/programs/mpl-core/src/plugins/burn.rs +++ b/programs/mpl-core/src/plugins/burn.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{account_info::AccountInfo, program_error::ProgramError}; use crate::{ - processor::{CompressArgs, CreateArgs, DecompressArgs, TransferArgs}, + processor::{CompressArgs, CreateArgs, DecompressArgs}, state::{Authority, DataBlob}, }; @@ -73,7 +73,6 @@ impl PluginValidation for Burn { &self, _authority: &AccountInfo, _new_owner: &AccountInfo, - _args: &TransferArgs, _authorities: &[Authority], ) -> Result { Ok(ValidationResult::Pass) diff --git a/programs/mpl-core/src/plugins/freeze.rs b/programs/mpl-core/src/plugins/freeze.rs index 779a85e7..aaacd429 100644 --- a/programs/mpl-core/src/plugins/freeze.rs +++ b/programs/mpl-core/src/plugins/freeze.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{account_info::AccountInfo, program_error::ProgramError}; use crate::{ - processor::{CompressArgs, CreateArgs, DecompressArgs, TransferArgs}, + processor::{CompressArgs, CreateArgs, DecompressArgs}, state::{Asset, Authority, DataBlob}, }; @@ -98,7 +98,6 @@ impl PluginValidation for Freeze { &self, _authority: &AccountInfo, _new_owner: &AccountInfo, - _args: &TransferArgs, _authorities: &[Authority], ) -> Result { if self.frozen { diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 5aa4b948..99722341 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -1,19 +1,21 @@ +use std::collections::BTreeMap; + use solana_program::{account_info::AccountInfo, program_error::ProgramError}; use crate::{ error::MplCoreError, processor::{ - AddPluginAuthorityArgs, CompressArgs, CreateArgs, DecompressArgs, - RemovePluginAuthorityArgs, TransferArgs, + AddPluginAuthorityArgs, CompressArgs, CreateArgs, DecompressArgs, RemovePluginAuthorityArgs, }, - state::{Asset, Authority}, + state::{Asset, Authority, Key}, }; -use super::{Plugin, PluginType}; +use super::{Plugin, PluginType, RegistryRecord}; /// Lifecycle permissions /// Plugins use this field to indicate their permission to approve or deny /// a lifecycle action. +#[derive(Eq, PartialEq, Copy, Clone, Debug)] pub enum CheckResult { /// A plugin is permitted to approve a lifecycle action. CanApprove, @@ -162,23 +164,20 @@ impl Plugin { &self, authority: &AccountInfo, new_owner: &AccountInfo, - args: &TransferArgs, authorities: &[Authority], ) -> Result { match self { Plugin::Reserved => Err(MplCoreError::InvalidPlugin.into()), Plugin::Royalties(royalties) => { - royalties.validate_transfer(authority, new_owner, args, authorities) - } - Plugin::Freeze(freeze) => { - freeze.validate_transfer(authority, new_owner, args, authorities) + royalties.validate_transfer(authority, new_owner, authorities) } - Plugin::Burn(burn) => burn.validate_transfer(authority, new_owner, args, authorities), + Plugin::Freeze(freeze) => freeze.validate_transfer(authority, new_owner, authorities), + Plugin::Burn(burn) => burn.validate_transfer(authority, new_owner, authorities), Plugin::Transfer(transfer) => { - transfer.validate_transfer(authority, new_owner, args, authorities) + transfer.validate_transfer(authority, new_owner, authorities) } Plugin::UpdateDelegate(update_delegate) => { - update_delegate.validate_transfer(authority, new_owner, args, authorities) + update_delegate.validate_transfer(authority, new_owner, authorities) } } } @@ -280,7 +279,7 @@ impl Plugin { /// Lifecycle validations /// Plugins utilize this to indicate whether they approve or reject a lifecycle action. -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Debug)] pub enum ValidationResult { /// The plugin approves the lifecycle action. Approved, @@ -339,7 +338,6 @@ pub(crate) trait PluginValidation { &self, authority: &AccountInfo, new_owner: &AccountInfo, - args: &TransferArgs, authorities: &[Authority], ) -> Result; @@ -379,3 +377,106 @@ pub(crate) trait PluginValidation { Ok(ValidationResult::Pass) } } + +pub(crate) fn validate_burn_plugin_checks<'a>( + key: Key, + checks: &BTreeMap, + authority: &AccountInfo<'a>, + asset: &AccountInfo<'a>, + collection: Option<&AccountInfo<'a>>, +) -> Result { + for (_, (check_key, check_result, registry_record)) in checks { + if *check_key == key + && matches!( + check_result, + CheckResult::CanApprove | CheckResult::CanReject + ) + { + let account = match key { + Key::Collection => collection.ok_or(MplCoreError::InvalidCollection)?, + Key::Asset => asset, + _ => unreachable!(), + }; + let result = Plugin::load(account, registry_record.offset)? + .validate_burn(authority, ®istry_record.authorities)?; + match result { + ValidationResult::Rejected => return Err(MplCoreError::InvalidAuthority.into()), + ValidationResult::Approved => return Ok(true), + ValidationResult::Pass => continue, + } + } + } + Ok(false) +} + +pub(crate) fn validate_transfer_plugin_checks<'a>( + key: Key, + checks: &BTreeMap, + authority: &AccountInfo<'a>, + new_owner: &AccountInfo<'a>, + asset: &AccountInfo<'a>, + collection: Option<&AccountInfo<'a>>, +) -> Result { + solana_program::msg!("validate_transfer_plugin_checks"); + for (_, (check_key, check_result, registry_record)) in checks { + if *check_key == key + && matches!( + check_result, + CheckResult::CanApprove | CheckResult::CanReject + ) + { + solana_program::msg!("key: {:?}", key); + let account = match key { + Key::Collection => collection.ok_or(MplCoreError::InvalidCollection)?, + Key::Asset => asset, + _ => unreachable!(), + }; + let result = Plugin::load(account, registry_record.offset)?.validate_transfer( + authority, + new_owner, + ®istry_record.authorities, + )?; + solana_program::msg!("result: {:?}", result); + match result { + ValidationResult::Rejected => return Err(MplCoreError::InvalidAuthority.into()), + ValidationResult::Approved => return Ok(true), + ValidationResult::Pass => continue, + } + } + } + Ok(false) +} + +pub(crate) fn validate_update_plugin_checks<'a>( + key: Key, + checks: &BTreeMap, + authority: &AccountInfo<'a>, + asset: &AccountInfo<'a>, + collection: Option<&AccountInfo<'a>>, +) -> Result { + solana_program::msg!("validate_update_plugin_checks"); + for (_, (check_key, check_result, registry_record)) in checks { + if *check_key == key + && matches!( + check_result, + CheckResult::CanApprove | CheckResult::CanReject + ) + { + solana_program::msg!("key: {:?}", key); + let account = match key { + Key::Collection => collection.ok_or(MplCoreError::InvalidCollection)?, + Key::Asset => asset, + _ => unreachable!(), + }; + let result = Plugin::load(account, registry_record.offset)? + .validate_update(authority, ®istry_record.authorities)?; + solana_program::msg!("result: {:?}", result); + match result { + ValidationResult::Rejected => return Err(MplCoreError::InvalidAuthority.into()), + ValidationResult::Approved => return Ok(true), + ValidationResult::Pass => continue, + } + } + } + Ok(false) +} diff --git a/programs/mpl-core/src/plugins/mod.rs b/programs/mpl-core/src/plugins/mod.rs index e1bb2ca4..fe66f9a2 100644 --- a/programs/mpl-core/src/plugins/mod.rs +++ b/programs/mpl-core/src/plugins/mod.rs @@ -87,10 +87,22 @@ impl Compressible for Plugin {} /// List of First Party Plugin types. #[repr(u16)] #[derive( - Clone, Copy, Debug, BorshSerialize, BorshDeserialize, Eq, PartialEq, ToPrimitive, EnumCount, + Clone, + Copy, + Debug, + BorshSerialize, + BorshDeserialize, + Eq, + PartialEq, + ToPrimitive, + EnumCount, + Default, + PartialOrd, + Ord, )] pub enum PluginType { /// Reserved plugin. + #[default] Reserved, /// Royalties plugin. Royalties, diff --git a/programs/mpl-core/src/plugins/plugin_registry.rs b/programs/mpl-core/src/plugins/plugin_registry.rs index 9241d6dc..7da940a3 100644 --- a/programs/mpl-core/src/plugins/plugin_registry.rs +++ b/programs/mpl-core/src/plugins/plugin_registry.rs @@ -1,10 +1,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::ShankAccount; -use std::cmp::Ordering; +use std::{cmp::Ordering, collections::BTreeMap}; use crate::state::{Authority, DataBlob, Key, SolanaAccount}; -use super::PluginType; +use super::{CheckResult, PluginType}; /// The Plugin Registry stores a record of all plugins, their location, and their authorities. #[repr(C)] @@ -18,6 +18,92 @@ pub struct PluginRegistry { pub external_plugins: Vec, // 4 } +impl PluginRegistry { + /// Evaluate create checks for all plugins. + pub fn check_create( + &self, + key: Key, + result: &mut BTreeMap, + ) { + for record in &self.registry { + result.insert( + record.plugin_type, + (key, record.plugin_type.check_create(), record.clone()), + ); + } + } + + /// Evaluate update checks for all plugins. + pub fn check_update( + &self, + key: Key, + result: &mut BTreeMap, + ) { + for record in &self.registry { + result.insert( + record.plugin_type, + (key, record.plugin_type.check_update(), record.clone()), + ); + } + } + + /// Evaluate delete checks for all plugins. + pub fn check_burn( + &self, + key: Key, + result: &mut BTreeMap, + ) { + for record in &self.registry { + result.insert( + record.plugin_type, + (key, record.plugin_type.check_burn(), record.clone()), + ); + } + } + + /// Evaluate transfer checks for all plugins. + pub fn check_transfer( + &self, + key: Key, + result: &mut BTreeMap, + ) { + for record in &self.registry { + result.insert( + record.plugin_type, + (key, record.plugin_type.check_transfer(), record.clone()), + ); + } + } + + /// Evaluate the compress checks for all plugins. + pub fn check_compress( + &self, + key: Key, + result: &mut BTreeMap, + ) { + for record in &self.registry { + result.insert( + record.plugin_type, + (key, record.plugin_type.check_compress(), record.clone()), + ); + } + } + + /// Evaluate the decompress checks for all plugins. + pub fn check_decompress( + &self, + key: Key, + result: &mut BTreeMap, + ) { + for record in &self.registry { + result.insert( + record.plugin_type, + (key, record.plugin_type.check_decompress(), record.clone()), + ); + } + } +} + impl DataBlob for PluginRegistry { fn get_initial_size() -> usize { 9 @@ -36,7 +122,7 @@ impl SolanaAccount for PluginRegistry { /// A simple type to store the mapping of Plugin type to Plugin data. #[repr(C)] -#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Default)] pub struct RegistryRecord { /// The type of plugin. pub plugin_type: PluginType, diff --git a/programs/mpl-core/src/plugins/royalties.rs b/programs/mpl-core/src/plugins/royalties.rs index 4de49c43..a66d8258 100644 --- a/programs/mpl-core/src/plugins/royalties.rs +++ b/programs/mpl-core/src/plugins/royalties.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ - processor::{CompressArgs, CreateArgs, DecompressArgs, TransferArgs}, + processor::{CompressArgs, CreateArgs, DecompressArgs}, state::Authority, }; @@ -67,12 +67,12 @@ impl PluginValidation for Royalties { &self, authority: &AccountInfo, new_owner: &AccountInfo, - _args: &TransferArgs, _authorities: &[Authority], ) -> Result { match &self.rule_set { RuleSet::None => Ok(ValidationResult::Pass), RuleSet::ProgramAllowList(allow_list) => { + solana_program::msg!("Evaluating royalties"); if allow_list.contains(authority.owner) || allow_list.contains(new_owner.owner) { Ok(ValidationResult::Pass) } else { diff --git a/programs/mpl-core/src/plugins/transfer.rs b/programs/mpl-core/src/plugins/transfer.rs index 63d415f8..a80b15b2 100644 --- a/programs/mpl-core/src/plugins/transfer.rs +++ b/programs/mpl-core/src/plugins/transfer.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{account_info::AccountInfo, program_error::ProgramError}; use crate::{ - processor::{CompressArgs, CreateArgs, DecompressArgs, TransferArgs}, + processor::{CompressArgs, CreateArgs, DecompressArgs}, state::{Authority, DataBlob}, }; @@ -73,7 +73,6 @@ impl PluginValidation for Transfer { &self, authority: &AccountInfo, _new_owner: &AccountInfo, - _args: &TransferArgs, authorities: &[Authority], ) -> Result { if authorities.contains(&Authority::Pubkey { diff --git a/programs/mpl-core/src/plugins/update_delegate.rs b/programs/mpl-core/src/plugins/update_delegate.rs index 23b34bd3..5cca5b58 100644 --- a/programs/mpl-core/src/plugins/update_delegate.rs +++ b/programs/mpl-core/src/plugins/update_delegate.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{account_info::AccountInfo, program_error::ProgramError}; use crate::{ - processor::{CompressArgs, CreateArgs, DecompressArgs, TransferArgs}, + processor::{CompressArgs, CreateArgs, DecompressArgs}, state::{Authority, DataBlob}, }; @@ -67,7 +67,6 @@ impl PluginValidation for UpdateDelegate { &self, _authority: &AccountInfo, _new_owner: &AccountInfo, - _args: &TransferArgs, _authorities: &[Authority], ) -> Result { Ok(ValidationResult::Pass) diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index 07b4b8d2..7810b7ed 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -1,13 +1,17 @@ +use std::collections::BTreeMap; + use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; -use strum::EnumCount; use crate::{ error::MplCoreError, instruction::accounts::{BurnAccounts, BurnCollectionAccounts}, - plugins::{CheckResult, Plugin, PluginType, ValidationResult}, - state::{Asset, Collection, Compressible, CompressionProof, Key}, + plugins::{ + validate_burn_plugin_checks, CheckResult, Plugin, PluginType, RegistryRecord, + ValidationResult, + }, + state::{Asset, Collection, Compressible, CompressionProof, Key, SolanaAccount}, utils::{close_program_account, fetch_core_data, load_key, verify_proof}, }; @@ -42,41 +46,77 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnArgs) -> Progr asset.wrap()?; } Key::Asset => { - let (asset, _, plugin_registry) = fetch_core_data::(ctx.accounts.asset)?; - - let mut approved = false; - match Asset::check_transfer() { - CheckResult::CanApprove | CheckResult::CanReject => { - match asset.validate_burn(&ctx.accounts)? { - ValidationResult::Approved => { - approved = true; - } - ValidationResult::Rejected => { - return Err(MplCoreError::InvalidAuthority.into()) - } - ValidationResult::Pass => (), - } - } - CheckResult::None => (), + let (_, _, plugin_registry) = fetch_core_data::(ctx.accounts.asset)?; + + let mut checks: BTreeMap = + BTreeMap::new(); + + // The asset approval overrides the collection approval. + let asset_approval = Asset::check_burn(); + let core_check = match asset_approval { + CheckResult::None => (Key::Collection, Collection::check_burn()), + _ => (Key::Asset, asset_approval), }; - if let Some(plugin_registry) = plugin_registry { - for record in plugin_registry.registry { - if matches!( - record.plugin_type.check_transfer(), - CheckResult::CanApprove | CheckResult::CanReject - ) { - let result = Plugin::load(ctx.accounts.asset, record.offset)? - .validate_burn(ctx.accounts.authority, &record.authorities)?; - if result == ValidationResult::Rejected { - return Err(MplCoreError::InvalidAuthority.into()); - } else if result == ValidationResult::Approved { - approved = true; - } + // Check the collection plugins first. + ctx.accounts.collection.and_then(|collection_info| { + fetch_core_data::(collection_info) + .map(|(_, _, registry)| { + registry.map(|r| { + r.check_burn(Key::Collection, &mut checks); + r + }) + }) + .ok()? + }); + + // Next check the asset plugins. Plugins on the asset override the collection plugins, + // so we don't need to validate the collection plugins if the asset has a plugin. + if let Some(registry) = plugin_registry.as_ref() { + registry.check_burn(Key::Asset, &mut checks); + } + + solana_program::msg!("checks: {:#?}", checks); + + // Do the core validation. + let mut approved = matches!( + core_check, + ( + Key::Asset | Key::Collection, + CheckResult::CanApprove | CheckResult::CanReject + ) + ) && { + (match core_check.0 { + Key::Collection => Collection::load( + ctx.accounts + .collection + .ok_or(MplCoreError::InvalidCollection)?, + 0, + )? + .validate_burn(ctx.accounts.authority)?, + Key::Asset => { + Asset::load(ctx.accounts.asset, 0)?.validate_burn(ctx.accounts.authority)? } - } + _ => return Err(MplCoreError::IncorrectAccount.into()), + }) == ValidationResult::Approved }; + approved = validate_burn_plugin_checks( + Key::Collection, + &checks, + ctx.accounts.authority, + ctx.accounts.asset, + ctx.accounts.collection, + )? || approved; + + approved = validate_burn_plugin_checks( + Key::Asset, + &checks, + ctx.accounts.authority, + ctx.accounts.asset, + ctx.accounts.collection, + )? || approved; + if !approved { return Err(MplCoreError::InvalidAuthority.into()); } @@ -112,7 +152,7 @@ pub(crate) fn burn_collection<'a>( let mut approved = false; match Collection::check_burn() { CheckResult::CanApprove | CheckResult::CanReject => { - match collection.validate_burn(&ctx.accounts)? { + match collection.validate_burn(ctx.accounts.authority)? { ValidationResult::Approved => { approved = true; } diff --git a/programs/mpl-core/src/processor/remove_plugin_authority.rs b/programs/mpl-core/src/processor/remove_plugin_authority.rs index 81b065cf..f6b81735 100644 --- a/programs/mpl-core/src/processor/remove_plugin_authority.rs +++ b/programs/mpl-core/src/processor/remove_plugin_authority.rs @@ -48,11 +48,13 @@ pub(crate) fn remove_plugin_authority<'a>( if collection_info.key != &collection_address { return Err(MplCoreError::InvalidCollection.into()); } - let collection = Collection::load(collection_info, 0)?; + let collection: Collection = Collection::load(collection_info, 0)?; if ctx.accounts.authority.key == &collection.update_authority { Authority::UpdateAuthority } else { - return Err(MplCoreError::InvalidAuthority.into()); + Authority::Pubkey { + address: *ctx.accounts.authority.key, + } } } None => return Err(MplCoreError::InvalidCollection.into()), diff --git a/programs/mpl-core/src/processor/transfer.rs b/programs/mpl-core/src/processor/transfer.rs index e7d40370..7b45ab9a 100644 --- a/programs/mpl-core/src/processor/transfer.rs +++ b/programs/mpl-core/src/processor/transfer.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; @@ -5,8 +7,10 @@ use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult}; use crate::{ error::MplCoreError, instruction::accounts::TransferAccounts, - plugins::{CheckResult, Plugin, ValidationResult}, - state::{Asset, Compressible, CompressionProof, HashedAsset, Key, SolanaAccount}, + plugins::{ + validate_transfer_plugin_checks, CheckResult, PluginType, RegistryRecord, ValidationResult, + }, + state::{Asset, Collection, Compressible, CompressionProof, HashedAsset, Key, SolanaAccount}, utils::{fetch_core_data, load_key, verify_proof}, }; @@ -47,80 +51,82 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferArgs) HashedAsset::new(asset.hash()?).save(ctx.accounts.asset, 0) } Key::Asset => { - // let mut asset = Asset::load(ctx.accounts.asset, 0)?; - - // let mut authority_check: Result<(), ProgramError> = - // Err(MplCoreError::InvalidAuthority.into()); - // if asset.get_size() != ctx.accounts.asset.data_len() { - // solana_program::msg!("Fetch Plugin"); - // let (authorities, plugin, _) = - // fetch_plugin(ctx.accounts.asset, PluginType::Delegate)?; - - // solana_program::msg!("Assert authority"); - // authority_check = assert_authority(&asset, ctx.accounts.authority, &authorities); - - // if let Plugin::Delegate(delegate) = plugin { - // if delegate.frozen { - // return Err(MplCoreError::AssetIsFrozen.into()); - // } - // } - // } - - // match authority_check { - // Ok(_) => Ok::<(), ProgramError>(()), - // Err(_) => { - // if ctx.accounts.authority.key != &asset.owner { - // Err(MplCoreError::InvalidAuthority.into()) - // } else { - // Ok(()) - // } - // } - // }?; - let (mut asset, _, plugin_registry) = fetch_core_data::(ctx.accounts.asset)?; - let mut approved = false; - match Asset::check_transfer() { - CheckResult::CanApprove | CheckResult::CanReject => { - match asset.validate_transfer(&ctx.accounts)? { - ValidationResult::Approved => { - approved = true; - } - ValidationResult::Rejected => { - return Err(MplCoreError::InvalidAuthority.into()) - } - ValidationResult::Pass => (), - } - } - CheckResult::None => (), + let mut checks: BTreeMap = + BTreeMap::new(); + + // The asset approval overrides the collection approval. + let asset_approval = Asset::check_transfer(); + let core_check = match asset_approval { + CheckResult::None => (Key::Collection, Collection::check_transfer()), + _ => (Key::Asset, asset_approval), }; - if let Some(plugin_registry) = plugin_registry { - for record in plugin_registry.registry { - if matches!( - record.plugin_type.check_transfer(), - CheckResult::CanApprove | CheckResult::CanReject - ) { - let result = Plugin::load(ctx.accounts.asset, record.offset)? - .validate_transfer( - ctx.accounts.authority, - ctx.accounts.new_owner, - &args, - &record.authorities, - )?; - if result == ValidationResult::Rejected { - return Err(MplCoreError::InvalidAuthority.into()); - } else if result == ValidationResult::Approved { - approved = true; - } - } - } + // Check the collection plugins first. + ctx.accounts.collection.and_then(|collection_info| { + fetch_core_data::(collection_info) + .map(|(_, _, registry)| { + registry.map(|r| { + r.check_transfer(Key::Collection, &mut checks); + r + }) + }) + .ok()? + }); + + // Next check the asset plugins. Plugins on the asset override the collection plugins, + // so we don't need to validate the collection plugins if the asset has a plugin. + if let Some(registry) = plugin_registry.as_ref() { + registry.check_transfer(Key::Asset, &mut checks); + } + + solana_program::msg!("checks: {:#?}", checks); + + // Do the core validation. + let mut approved = matches!( + core_check, + ( + Key::Asset | Key::Collection, + CheckResult::CanApprove | CheckResult::CanReject + ) + ) && { + (match core_check.0 { + Key::Collection => Collection::load( + ctx.accounts + .collection + .ok_or(MplCoreError::InvalidCollection)?, + 0, + )? + .validate_transfer()?, + Key::Asset => Asset::load(ctx.accounts.asset, 0)? + .validate_transfer(ctx.accounts.authority)?, + _ => return Err(MplCoreError::IncorrectAccount.into()), + }) == ValidationResult::Approved }; + solana_program::msg!("approved: {:#?}", approved); + + approved = validate_transfer_plugin_checks( + Key::Collection, + &checks, + ctx.accounts.authority, + ctx.accounts.new_owner, + ctx.accounts.asset, + ctx.accounts.collection, + )? || approved; + + approved = validate_transfer_plugin_checks( + Key::Asset, + &checks, + ctx.accounts.authority, + ctx.accounts.new_owner, + ctx.accounts.asset, + ctx.accounts.collection, + )? || approved; if !approved { return Err(MplCoreError::InvalidAuthority.into()); } - asset.owner = *ctx.accounts.new_owner.key; asset.save(ctx.accounts.asset, 0) } diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index 15abb889..785aa257 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use borsh::{BorshDeserialize, BorshSerialize}; use mpl_utils::assert_signer; use solana_program::{ @@ -7,8 +9,11 @@ use solana_program::{ use crate::{ error::MplCoreError, instruction::accounts::{UpdateAccounts, UpdateCollectionAccounts}, - plugins::{CheckResult, Plugin, RegistryRecord, ValidationResult}, - state::{Asset, Collection, DataBlob, SolanaAccount, UpdateAuthority}, + plugins::{ + validate_update_plugin_checks, CheckResult, Plugin, PluginType, RegistryRecord, + ValidationResult, + }, + state::{Asset, Collection, DataBlob, Key, SolanaAccount, UpdateAuthority}, utils::{fetch_core_data, resize_or_reallocate_account}, }; @@ -36,34 +41,74 @@ pub(crate) fn update<'a>(accounts: &'a [AccountInfo<'a>], args: UpdateArgs) -> P let (mut asset, plugin_header, plugin_registry) = fetch_core_data::(ctx.accounts.asset)?; let asset_size = asset.get_size() as isize; - let mut approved = false; - match Asset::check_update() { - CheckResult::CanApprove => { - if asset.validate_update(&ctx.accounts)? == ValidationResult::Approved { - approved = true; - } - } - CheckResult::CanReject => return Err(MplCoreError::InvalidAuthority.into()), - CheckResult::None => (), + let mut checks: BTreeMap = BTreeMap::new(); + + // The asset approval overrides the collection approval. + let asset_approval = Asset::check_update(); + let core_check = match asset_approval { + CheckResult::None => (Key::Collection, Collection::check_update()), + _ => (Key::Asset, asset_approval), }; - if let Some(plugin_registry) = plugin_registry.clone() { - for record in plugin_registry.registry { - if matches!( - record.plugin_type.check_transfer(), - CheckResult::CanApprove | CheckResult::CanReject - ) { - let result = Plugin::load(ctx.accounts.asset, record.offset)? - .validate_update(ctx.accounts.authority, &record.authorities)?; - if result == ValidationResult::Rejected { - return Err(MplCoreError::InvalidAuthority.into()); - } else if result == ValidationResult::Approved { - approved = true; - } + // Check the collection plugins first. + ctx.accounts.collection.and_then(|collection_info| { + fetch_core_data::(collection_info) + .map(|(_, _, registry)| { + registry.map(|r| { + r.check_update(Key::Collection, &mut checks); + r + }) + }) + .ok()? + }); + + // Next check the asset plugins. Plugins on the asset override the collection plugins, + // so we don't need to validate the collection plugins if the asset has a plugin. + if let Some(registry) = plugin_registry.as_ref() { + registry.check_update(Key::Asset, &mut checks); + } + + solana_program::msg!("checks: {:#?}", checks); + + // Do the core validation. + let mut approved = matches!( + core_check, + ( + Key::Asset | Key::Collection, + CheckResult::CanApprove | CheckResult::CanReject + ) + ) && { + (match core_check.0 { + Key::Collection => Collection::load( + ctx.accounts + .collection + .ok_or(MplCoreError::InvalidCollection)?, + 0, + )? + .validate_update(ctx.accounts.authority)?, + Key::Asset => { + Asset::load(ctx.accounts.asset, 0)?.validate_update(ctx.accounts.authority)? } - } + _ => return Err(MplCoreError::IncorrectAccount.into()), + }) == ValidationResult::Approved }; + approved = validate_update_plugin_checks( + Key::Collection, + &checks, + ctx.accounts.authority, + ctx.accounts.asset, + ctx.accounts.collection, + )? || approved; + + approved = validate_update_plugin_checks( + Key::Asset, + &checks, + ctx.accounts.authority, + ctx.accounts.asset, + ctx.accounts.collection, + )? || approved; + if !approved { return Err(MplCoreError::InvalidAuthority.into()); } @@ -184,7 +229,7 @@ pub(crate) fn update_collection<'a>( let mut approved = false; match Asset::check_update() { CheckResult::CanApprove => { - if asset.validate_update(&ctx.accounts)? == ValidationResult::Approved { + if asset.validate_update(&ctx.accounts.authority)? == ValidationResult::Approved { approved = true; } } diff --git a/programs/mpl-core/src/state/asset.rs b/programs/mpl-core/src/state/asset.rs index 44e51f13..40e7223f 100644 --- a/programs/mpl-core/src/state/asset.rs +++ b/programs/mpl-core/src/state/asset.rs @@ -1,11 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::ShankAccount; -use solana_program::{program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ - instruction::accounts::{ - BurnAccounts, CompressAccounts, DecompressAccounts, TransferAccounts, UpdateAccounts, - }, + instruction::accounts::{CompressAccounts, DecompressAccounts}, plugins::{CheckResult, ValidationResult}, state::{Compressible, CompressionProof, DataBlob, Key, SolanaAccount}, }; @@ -36,6 +34,11 @@ impl Asset { CheckResult::CanApprove } + /// Check permissions for the burn lifecycle event. + pub fn check_burn() -> CheckResult { + CheckResult::CanApprove + } + /// Check permissions for the update lifecycle event. pub fn check_update() -> CheckResult { CheckResult::CanApprove @@ -52,8 +55,11 @@ impl Asset { } /// Validate the update lifecycle event. - pub fn validate_update(&self, ctx: &UpdateAccounts) -> Result { - if ctx.authority.key == &self.update_authority.key() { + pub fn validate_update( + &self, + authority: &AccountInfo, + ) -> Result { + if authority.key == &self.update_authority.key() { Ok(ValidationResult::Approved) } else { Ok(ValidationResult::Pass) @@ -61,8 +67,8 @@ impl Asset { } /// Validate the burn lifecycle event. - pub fn validate_burn(&self, ctx: &BurnAccounts) -> Result { - if ctx.authority.key == &self.owner { + pub fn validate_burn(&self, authority: &AccountInfo) -> Result { + if authority.key == &self.owner { Ok(ValidationResult::Approved) } else { Ok(ValidationResult::Pass) @@ -72,9 +78,9 @@ impl Asset { /// Validate the transfer lifecycle event. pub fn validate_transfer( &self, - ctx: &TransferAccounts, + authority: &AccountInfo, ) -> Result { - if ctx.authority.key == &self.owner { + if authority.key == &self.owner { Ok(ValidationResult::Approved) } else { Ok(ValidationResult::Pass) diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index 7ebcc636..88e92413 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -1,11 +1,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use shank::ShankAccount; -use solana_program::{program_error::ProgramError, pubkey::Pubkey}; +use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; -use crate::{ - instruction::accounts::{BurnCollectionAccounts, UpdateCollectionAccounts}, - plugins::{CheckResult, ValidationResult}, -}; +use crate::plugins::{CheckResult, ValidationResult}; use super::{CoreAsset, DataBlob, Key, SolanaAccount, UpdateAuthority}; @@ -73,12 +70,17 @@ impl Collection { CheckResult::None } + /// Validate the transfer lifecycle event. + pub fn validate_transfer(&self) -> Result { + Ok(ValidationResult::Pass) + } + /// Validate the update lifecycle event. pub fn validate_update( &self, - ctx: &UpdateCollectionAccounts, + authority: &AccountInfo, ) -> Result { - if ctx.authority.key == &self.update_authority { + if authority.key == &self.update_authority { Ok(ValidationResult::Approved) } else { Ok(ValidationResult::Pass) @@ -86,11 +88,8 @@ impl Collection { } /// Validate the burn lifecycle event. - pub fn validate_burn( - &self, - ctx: &BurnCollectionAccounts, - ) -> Result { - if ctx.authority.key == &self.update_authority { + pub fn validate_burn(&self, authority: &AccountInfo) -> Result { + if authority.key == &self.update_authority { Ok(ValidationResult::Approved) } else { Ok(ValidationResult::Pass)