diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index d10c8a7d..4fd32216 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -330,17 +330,30 @@ export class NotAvailableError extends ProgramError { codeToErrorMap.set(0x17, NotAvailableError); nameToErrorMap.set('NotAvailable', NotAvailableError); +/** InvalidAsset: Invalid Asset passed in */ +export class InvalidAssetError extends ProgramError { + override readonly name: string = 'InvalidAsset'; + + readonly code: number = 0x18; // 24 + + constructor(program: Program, cause?: Error) { + super('Invalid Asset passed in', program, cause); + } +} +codeToErrorMap.set(0x18, InvalidAssetError); +nameToErrorMap.set('InvalidAsset', InvalidAssetError); + /** MissingCollection: Missing collection */ export class MissingCollectionError extends ProgramError { override readonly name: string = 'MissingCollection'; - readonly code: number = 0x18; // 24 + readonly code: number = 0x19; // 25 constructor(program: Program, cause?: Error) { super('Missing collection', program, cause); } } -codeToErrorMap.set(0x18, MissingCollectionError); +codeToErrorMap.set(0x19, MissingCollectionError); nameToErrorMap.set('MissingCollection', MissingCollectionError); /** diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 3896fae0..8b6009fc 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -82,7 +82,10 @@ pub enum MplCoreError { /// 23 (0x17) - Feature not available #[error("Feature not available")] NotAvailable, - /// 24 (0x18) - Missing collection + /// 24 (0x18) - Invalid Asset passed in + #[error("Invalid Asset passed in")] + InvalidAsset, + /// 25 (0x19) - Missing collection #[error("Missing collection")] MissingCollection, } diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 804f4ca0..4a0bd1a1 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -2372,6 +2372,11 @@ }, { "code": 24, + "name": "InvalidAsset", + "msg": "Invalid Asset passed in" + }, + { + "code": 25, "name": "MissingCollection", "msg": "Missing collection" } diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index fa39e3b1..1e7ec414 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -105,7 +105,11 @@ pub enum MplCoreError { #[error("Feature not available")] NotAvailable, - /// 24 - Missing collection + /// 24 - Invalid Asset passed in + #[error("Invalid Asset passed in")] + InvalidAsset, + + /// 25 - Missing collection #[error("Missing collection")] MissingCollection, } diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 06bef152..04cb35cf 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -20,6 +20,8 @@ pub enum CheckResult { CanReject, /// A plugin is not permitted to approve or reject a lifecycle action. None, + /// Certain plugins can force approve a lifecycle action. + CanForceApprove, } impl PluginType { @@ -418,6 +420,8 @@ pub enum ValidationResult { Rejected, /// The plugin abstains from approving or rejecting the lifecycle action. Pass, + /// The plugin force approves the lifecycle action. + ForceApproved, } /// Plugin validation trait which is implemented by each plugin. @@ -537,6 +541,9 @@ pub(crate) trait PluginValidation { } } +/// This function iterates through all plugin checks passed in and performs the validation +/// by deserializing and calling validate on the plugin. +/// The STRONGEST result is returned. #[allow(clippy::too_many_arguments, clippy::type_complexity)] pub(crate) fn validate_plugin_checks<'a>( key: Key, @@ -544,7 +551,7 @@ pub(crate) fn validate_plugin_checks<'a>( authority: &AccountInfo<'a>, new_owner: Option<&AccountInfo>, new_plugin: Option<&Plugin>, - asset: &AccountInfo<'a>, + asset: Option<&AccountInfo<'a>>, collection: Option<&AccountInfo<'a>>, validate_fp: fn( &Plugin, @@ -553,8 +560,10 @@ pub(crate) fn validate_plugin_checks<'a>( &Authority, Option<&Plugin>, ) -> Result, -) -> Result { - for (_, (check_key, check_result, registry_record)) in checks { +) -> Result { + let mut approved = false; + let mut rejected = false; + for (check_key, check_result, registry_record) in checks.values() { if *check_key == key && matches!( check_result, @@ -564,7 +573,7 @@ pub(crate) fn validate_plugin_checks<'a>( solana_program::msg!("Validating plugin checks"); let account = match key { Key::Collection => collection.ok_or(MplCoreError::InvalidCollection)?, - Key::Asset => asset, + Key::Asset => asset.ok_or(MplCoreError::InvalidAsset)?, _ => unreachable!(), }; @@ -576,11 +585,19 @@ pub(crate) fn validate_plugin_checks<'a>( new_plugin, )?; match result { - ValidationResult::Rejected => return Err(MplCoreError::InvalidAuthority.into()), - ValidationResult::Approved => return Ok(true), + ValidationResult::Rejected => rejected = true, + ValidationResult::Approved => approved = true, ValidationResult::Pass => continue, + ValidationResult::ForceApproved => return Ok(ValidationResult::ForceApproved), } } } - Ok(false) + + if rejected { + Ok(ValidationResult::Rejected) + } else if approved { + Ok(ValidationResult::Approved) + } else { + Ok(ValidationResult::Pass) + } } diff --git a/programs/mpl-core/src/plugins/plugin_registry.rs b/programs/mpl-core/src/plugins/plugin_registry.rs index b1d3c52b..7a62cc4a 100644 --- a/programs/mpl-core/src/plugins/plugin_registry.rs +++ b/programs/mpl-core/src/plugins/plugin_registry.rs @@ -20,7 +20,7 @@ pub struct PluginRegistry { impl PluginRegistry { /// Evaluate checks for all plugins in the registry. - pub fn check_registry( + pub(crate) fn check_registry( &self, key: Key, check_fp: fn(&PluginType) -> CheckResult, diff --git a/programs/mpl-core/src/processor/transfer.rs b/programs/mpl-core/src/processor/transfer.rs index e36d7f53..47e79da9 100644 --- a/programs/mpl-core/src/processor/transfer.rs +++ b/programs/mpl-core/src/processor/transfer.rs @@ -9,7 +9,7 @@ use crate::{ state::{Asset, Collection, CompressionProof, Key, SolanaAccount, Wrappable}, utils::{ compress_into_account_space, load_key, rebuild_account_state_from_proof_data, - validate_asset_permissions, verify_proof, + resolve_payer, validate_asset_permissions, verify_proof, }, }; @@ -25,12 +25,7 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferArgs) // Guards. assert_signer(ctx.accounts.authority)?; - let payer = if let Some(payer) = ctx.accounts.payer { - assert_signer(payer)?; - payer - } else { - ctx.accounts.authority - }; + let payer = resolve_payer(ctx.accounts.authority, ctx.accounts.payer)?; let key = load_key(ctx.accounts.asset, 0)?; diff --git a/programs/mpl-core/src/state/asset.rs b/programs/mpl-core/src/state/asset.rs index f9abe1d2..2b3c3b45 100644 --- a/programs/mpl-core/src/state/asset.rs +++ b/programs/mpl-core/src/state/asset.rs @@ -21,6 +21,7 @@ pub struct Asset { pub key: Key, //1 /// The owner of the asset. pub owner: Pubkey, //32 + //TODO: Fix this for dynamic size /// The update authority of the asset. pub update_authority: UpdateAuthority, //33 /// The name of the asset. diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs index 2eed04a5..66b5456b 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -273,14 +273,16 @@ pub fn validate_asset_permissions<'a>( solana_program::msg!("checks: {:#?}", checks); // Do the core validation. - let mut approved = matches!( + let mut approved = false; + let mut rejected = false; + if matches!( core_check, ( Key::Asset | Key::Collection, - CheckResult::CanApprove | CheckResult::CanReject + CheckResult::CanApprove | CheckResult::CanReject | CheckResult::CanForceApprove ) - ) && { - (match core_check.0 { + ) { + let result = match core_check.0 { Key::Collection => collection_validate_fp( &Collection::load(collection.ok_or(MplCoreError::InvalidCollection)?, 0)?, authority_info, @@ -288,41 +290,59 @@ pub fn validate_asset_permissions<'a>( )?, Key::Asset => asset_validate_fp(&Asset::load(asset, 0)?, authority_info, new_plugin)?, _ => return Err(MplCoreError::IncorrectAccount.into()), - }) == ValidationResult::Approved + }; + match result { + ValidationResult::Approved => approved = true, + ValidationResult::Rejected => rejected = true, + ValidationResult::Pass => (), + ValidationResult::ForceApproved => { + return Ok((deserialized_asset, plugin_header, plugin_registry)) + } + } }; - solana_program::msg!("approved: {:#?}", approved); + solana_program::msg!("approved: {:?} rejected {:?}", approved, rejected); - // Validations by plugins can both approve and deny an action (e.g. the Freeze plugin can reject - // a transfer because the token is frozen) so we always want to evaluate. That is why existing - // `approved` value is second in the OR statement. - approved = validate_plugin_checks( + match validate_plugin_checks( Key::Collection, &checks, authority_info, new_owner, new_plugin, - asset, + Some(asset), collection, plugin_validate_fp, - )? || approved; + )? { + ValidationResult::Approved => approved = true, + ValidationResult::Rejected => rejected = true, + ValidationResult::Pass => (), + ValidationResult::ForceApproved => { + return Ok((deserialized_asset, plugin_header, plugin_registry)) + } + }; - solana_program::msg!("approved: {:#?}", approved); + solana_program::msg!("approved: {:?} rejected {:?}", approved, rejected); - // Again we always want to evaluate the plugin checks so order of operations is important. - approved = validate_plugin_checks( + match validate_plugin_checks( Key::Asset, &checks, authority_info, new_owner, new_plugin, - asset, + Some(asset), collection, plugin_validate_fp, - )? || approved; + )? { + ValidationResult::Approved => approved = true, + ValidationResult::Rejected => rejected = true, + ValidationResult::Pass => (), + ValidationResult::ForceApproved => { + return Ok((deserialized_asset, plugin_header, plugin_registry)) + } + }; - solana_program::msg!("approved: {:#?}", approved); + solana_program::msg!("approved: {:?} rejected {:?}", approved, rejected); - if !approved { + if rejected || !approved { return Err(MplCoreError::InvalidAuthority.into()); } @@ -353,45 +373,65 @@ pub fn validate_collection_permissions<'a>( let (deserialized_collection, plugin_header, plugin_registry) = fetch_core_data::(collection)?; - // let checks: [(Key, CheckResult); PluginType::COUNT + 2]; + let mut checks: BTreeMap = BTreeMap::new(); + + let core_check = (Key::Collection, collection_check_fp()); + + // Check the collection plugins first. + if let Some(registry) = plugin_registry.as_ref() { + registry.check_registry(Key::Collection, plugin_check_fp, &mut checks); + } + + solana_program::msg!("checks: {:#?}", checks); + // Do the core validation. let mut approved = false; - match collection_check_fp() { - CheckResult::CanApprove | CheckResult::CanReject => { - match collection_validate_fp(&deserialized_collection, authority_info, new_plugin)? { - ValidationResult::Approved => { - approved = true; - } - ValidationResult::Rejected => return Err(MplCoreError::InvalidAuthority.into()), - ValidationResult::Pass => (), + let mut rejected = false; + if matches!( + core_check, + ( + Key::Collection, + CheckResult::CanApprove | CheckResult::CanReject | CheckResult::CanForceApprove + ) + ) { + let result = match core_check.0 { + Key::Collection => { + collection_validate_fp(&deserialized_collection, authority_info, new_plugin)? + } + _ => return Err(MplCoreError::IncorrectAccount.into()), + }; + match result { + ValidationResult::Approved => approved = true, + ValidationResult::Rejected => rejected = true, + ValidationResult::Pass => (), + ValidationResult::ForceApproved => { + return Ok((deserialized_collection, plugin_header, plugin_registry)) } } - CheckResult::None => (), }; + solana_program::msg!("approved: {:?} rejected {:?}", approved, rejected); - if let Some(plugin_registry) = &plugin_registry { - for record in plugin_registry.registry.iter() { - if matches!( - plugin_check_fp(&record.plugin_type), - CheckResult::CanApprove | CheckResult::CanReject - ) { - let result = plugin_validate_fp( - &Plugin::load(collection, record.offset)?, - authority_info, - None, - &record.authority, - new_plugin, - )?; - if result == ValidationResult::Rejected { - return Err(MplCoreError::InvalidAuthority.into()); - } else if result == ValidationResult::Approved { - approved = true; - } - } + match validate_plugin_checks( + Key::Collection, + &checks, + authority_info, + None, + new_plugin, + None, + Some(collection), + plugin_validate_fp, + )? { + ValidationResult::Approved => approved = true, + ValidationResult::Rejected => rejected = true, + ValidationResult::Pass => (), + ValidationResult::ForceApproved => { + return Ok((deserialized_collection, plugin_header, plugin_registry)) } }; - if !approved { + solana_program::msg!("approved: {:?} rejected {:?}", approved, rejected); + + if rejected || !approved { return Err(MplCoreError::InvalidAuthority.into()); }