Skip to content

Commit

Permalink
Merge pull request #15 from metaplex-foundation/feat/force-approve
Browse files Browse the repository at this point in the history
Adding force approve
  • Loading branch information
blockiosaurus authored Mar 12, 2024
2 parents 6e3a7ee + 5ea4aab commit 800b1c3
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 68 deletions.
17 changes: 15 additions & 2 deletions clients/js/src/generated/errors/mplCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand Down
5 changes: 4 additions & 1 deletion clients/rust/src/generated/errors/mpl_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
5 changes: 5 additions & 0 deletions idls/mpl_core.json
Original file line number Diff line number Diff line change
Expand Up @@ -2372,6 +2372,11 @@
},
{
"code": 24,
"name": "InvalidAsset",
"msg": "Invalid Asset passed in"
},
{
"code": 25,
"name": "MissingCollection",
"msg": "Missing collection"
}
Expand Down
6 changes: 5 additions & 1 deletion programs/mpl-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
31 changes: 24 additions & 7 deletions programs/mpl-core/src/plugins/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -537,14 +541,17 @@ 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,
checks: &BTreeMap<PluginType, (Key, CheckResult, RegistryRecord)>,
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,
Expand All @@ -553,8 +560,10 @@ pub(crate) fn validate_plugin_checks<'a>(
&Authority,
Option<&Plugin>,
) -> Result<ValidationResult, ProgramError>,
) -> Result<bool, ProgramError> {
for (_, (check_key, check_result, registry_record)) in checks {
) -> Result<ValidationResult, ProgramError> {
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,
Expand All @@ -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!(),
};

Expand All @@ -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)
}
}
2 changes: 1 addition & 1 deletion programs/mpl-core/src/plugins/plugin_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 2 additions & 7 deletions programs/mpl-core/src/processor/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};

Expand All @@ -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)?;

Expand Down
1 change: 1 addition & 0 deletions programs/mpl-core/src/state/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
138 changes: 89 additions & 49 deletions programs/mpl-core/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,56 +273,76 @@ 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,
new_plugin,
)?,
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());
}

Expand Down Expand Up @@ -353,45 +373,65 @@ pub fn validate_collection_permissions<'a>(
let (deserialized_collection, plugin_header, plugin_registry) =
fetch_core_data::<Collection>(collection)?;

// let checks: [(Key, CheckResult); PluginType::COUNT + 2];
let mut checks: BTreeMap<PluginType, (Key, CheckResult, RegistryRecord)> = 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());
}

Expand Down

0 comments on commit 800b1c3

Please sign in to comment.