From 14a52764e1645597f36864db3d5965265560f51c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 12:05:35 -0700 Subject: [PATCH 01/14] Regenerate IDL with changed error code --- clients/js/src/generated/errors/mplCore.ts | 19 ++++++++++--------- clients/rust/src/generated/errors/mpl_core.rs | 6 +++--- idls/mpl_core.json | 4 ++-- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 2d47f2e6..1c22df20 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -492,21 +492,22 @@ export class MissingExternalAccountError extends ProgramError { codeToErrorMap.set(0x22, MissingExternalAccountError); nameToErrorMap.set('MissingExternalAccount', MissingExternalAccountError); -/** InvalidExternalPluginSetting: Invalid setting for external plugin */ -export class InvalidExternalPluginSettingError extends ProgramError { - override readonly name: string = 'InvalidExternalPluginSetting'; +/** OracleCanDenyOnly: Oracle external plugin can only be configured to deny */ +export class OracleCanDenyOnlyError extends ProgramError { + override readonly name: string = 'OracleCanDenyOnly'; readonly code: number = 0x23; // 35 constructor(program: Program, cause?: Error) { - super('Invalid setting for external plugin', program, cause); + super( + 'Oracle external plugin can only be configured to deny', + program, + cause + ); } } -codeToErrorMap.set(0x23, InvalidExternalPluginSettingError); -nameToErrorMap.set( - 'InvalidExternalPluginSetting', - InvalidExternalPluginSettingError -); +codeToErrorMap.set(0x23, OracleCanDenyOnlyError); +nameToErrorMap.set('OracleCanDenyOnly', OracleCanDenyOnlyError); /** * Attempts to resolve a custom program error from the provided error code. diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 4dde2210..07d39ad3 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -115,9 +115,9 @@ pub enum MplCoreError { /// 34 (0x22) - Missing account needed for external plugin #[error("Missing account needed for external plugin")] MissingExternalAccount, - /// 35 (0x23) - Invalid setting for external plugin - #[error("Invalid setting for external plugin")] - InvalidExternalPluginSetting, + /// 35 (0x23) - Oracle external plugin can only be configured to deny + #[error("Oracle external plugin can only be configured to deny")] + OracleCanDenyOnly, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 7cc36358..5d5e903b 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -4166,8 +4166,8 @@ }, { "code": 35, - "name": "InvalidExternalPluginSetting", - "msg": "Invalid setting for external plugin" + "name": "OracleCanDenyOnly", + "msg": "Oracle external plugin can only be configured to deny" } ], "metadata": { From bc66ff70c3c80d83d1588015648e51e759a28627 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 12:36:55 -0700 Subject: [PATCH 02/14] Make check tighter to not allow zero checks Also fix Rust client tests --- clients/rust/tests/add_external_plugins.rs | 2 +- clients/rust/tests/create_with_external_plugins.rs | 2 +- clients/rust/tests/remove_external_plugins.rs | 2 +- clients/rust/tests/update_external_plugins.rs | 2 +- programs/mpl-core/src/plugins/utils.rs | 9 ++++----- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/clients/rust/tests/add_external_plugins.rs b/clients/rust/tests/add_external_plugins.rs index 06f0a154..c3c8e5a9 100644 --- a/clients/rust/tests/add_external_plugins.rs +++ b/clients/rust/tests/add_external_plugins.rs @@ -150,7 +150,7 @@ async fn test_add_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 1 }, + ExternalCheckResult { flags: 2 }, )]), pda: None, results_offset: None, diff --git a/clients/rust/tests/create_with_external_plugins.rs b/clients/rust/tests/create_with_external_plugins.rs index d3cbf194..44b98048 100644 --- a/clients/rust/tests/create_with_external_plugins.rs +++ b/clients/rust/tests/create_with_external_plugins.rs @@ -92,7 +92,7 @@ async fn test_create_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 1 }, + ExternalCheckResult { flags: 2 }, )]), pda: None, results_offset: None, diff --git a/clients/rust/tests/remove_external_plugins.rs b/clients/rust/tests/remove_external_plugins.rs index cdd9412e..85461cd0 100644 --- a/clients/rust/tests/remove_external_plugins.rs +++ b/clients/rust/tests/remove_external_plugins.rs @@ -127,7 +127,7 @@ async fn test_remove_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 1 }, + ExternalCheckResult { flags: 2 }, )]), pda: None, results_offset: None, diff --git a/clients/rust/tests/update_external_plugins.rs b/clients/rust/tests/update_external_plugins.rs index fa63f401..dd546743 100644 --- a/clients/rust/tests/update_external_plugins.rs +++ b/clients/rust/tests/update_external_plugins.rs @@ -140,7 +140,7 @@ async fn test_update_oracle() { init_plugin_authority: Some(PluginAuthority::UpdateAuthority), lifecycle_checks: Some(vec![( HookableLifecycleEvent::Transfer, - ExternalCheckResult { flags: 1 }, + ExternalCheckResult { flags: 2 }, )]), pda: None, results_offset: None, diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 53c3e201..8f2801ca 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -11,9 +11,8 @@ use crate::{ }; use super::{ - ExternalCheckResultBits, ExternalPlugin, ExternalPluginInitInfo, ExternalPluginKey, - ExternalPluginType, ExternalRegistryRecord, Plugin, PluginHeaderV1, PluginRegistryV1, - PluginType, RegistryRecord, + ExternalPlugin, ExternalPluginInitInfo, ExternalPluginKey, ExternalPluginType, + ExternalRegistryRecord, Plugin, PluginHeaderV1, PluginRegistryV1, PluginType, RegistryRecord, }; /// Create plugin header and registry if it doesn't exist @@ -334,8 +333,8 @@ pub fn initialize_external_plugin<'a, T: DataBlob + SolanaAccount>( // You cannot configure an Oracle plugin to approve lifecycle events. if let Some(lifecycle_checks) = &init_info.lifecycle_checks { for (_, result) in lifecycle_checks { - let result_bits = ExternalCheckResultBits::from(*result); - if result_bits.can_listen() || result_bits.can_approve() { + // Deny is bit 2. + if result.flags != 0x2 { return Err(MplCoreError::OracleCanDenyOnly.into()); } } From 16c4ac838df3ece1b3f074640b7dea33b7bf112a Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 13:47:22 -0700 Subject: [PATCH 03/14] Require Oracle to provide lifecycle checks at init --- .../mpl-core/src/plugins/external_plugins.rs | 15 +++++------ programs/mpl-core/src/plugins/oracle.rs | 2 +- programs/mpl-core/src/plugins/utils.rs | 26 ++++++++++--------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/programs/mpl-core/src/plugins/external_plugins.rs b/programs/mpl-core/src/plugins/external_plugins.rs index ac4c074f..dcdd5d50 100644 --- a/programs/mpl-core/src/plugins/external_plugins.rs +++ b/programs/mpl-core/src/plugins/external_plugins.rs @@ -108,15 +108,12 @@ impl ExternalPlugin { } } ExternalPluginInitInfo::Oracle(init_info) => { - if let Some(lifecycle_checks) = &init_info.lifecycle_checks { - if let Some(checks) = lifecycle_checks - .iter() - .find(|event| event.0 == HookableLifecycleEvent::Create) - { - checks.1 - } else { - ExternalCheckResult::none() - } + if let Some(checks) = &init_info + .lifecycle_checks + .iter() + .find(|event| event.0 == HookableLifecycleEvent::Create) + { + checks.1 } else { ExternalCheckResult::none() } diff --git a/programs/mpl-core/src/plugins/oracle.rs b/programs/mpl-core/src/plugins/oracle.rs index 97f45931..28155a8a 100644 --- a/programs/mpl-core/src/plugins/oracle.rs +++ b/programs/mpl-core/src/plugins/oracle.rs @@ -129,7 +129,7 @@ pub struct OracleInitInfo { /// Initial plugin authority. pub init_plugin_authority: Option, /// The lifecyle events for which the the external plugin is active. - pub lifecycle_checks: Option>, + pub lifecycle_checks: Vec<(HookableLifecycleEvent, ExternalCheckResult)>, /// Optional PDA (derived from Pubkey attached to `ExternalPluginKey`). pub pda: Option, /// Optional offset for validation results struct used in Oracle account. Default diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 8f2801ca..89ad8c22 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -325,24 +325,26 @@ pub fn initialize_external_plugin<'a, T: DataBlob + SolanaAccount>( } } - let (authority, lifecycle_checks) = match &init_info { - ExternalPluginInitInfo::LifecycleHook(init_info) => { - (init_info.init_plugin_authority, &init_info.lifecycle_checks) - } + let (authority, lifecycle_checks) = match init_info { + ExternalPluginInitInfo::LifecycleHook(init_info) => ( + init_info.init_plugin_authority, + init_info.lifecycle_checks.clone(), + ), ExternalPluginInitInfo::Oracle(init_info) => { // You cannot configure an Oracle plugin to approve lifecycle events. - if let Some(lifecycle_checks) = &init_info.lifecycle_checks { - for (_, result) in lifecycle_checks { - // Deny is bit 2. - if result.flags != 0x2 { - return Err(MplCoreError::OracleCanDenyOnly.into()); - } + for (_, result) in &init_info.lifecycle_checks { + // Deny is bit 2. + if result.flags != 0x2 { + return Err(MplCoreError::OracleCanDenyOnly.into()); } } - (init_info.init_plugin_authority, &init_info.lifecycle_checks) + ( + init_info.init_plugin_authority, + Some(init_info.lifecycle_checks.clone()), + ) } - ExternalPluginInitInfo::DataStore(init_info) => (init_info.init_plugin_authority, &None), + ExternalPluginInitInfo::DataStore(init_info) => (init_info.init_plugin_authority, None), }; let old_registry_offset = plugin_header.plugin_registry_offset; From 1da010d466b75bb91e46c46579b3fd28b61a33c5 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 13:48:50 -0700 Subject: [PATCH 04/14] Regenerate IDL and clients --- .../src/generated/types/baseOracleInitInfo.ts | 18 +++++++---------- .../src/generated/types/oracle_init_info.rs | 2 +- idls/mpl_core.json | 20 +++++++++---------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/clients/js/src/generated/types/baseOracleInitInfo.ts b/clients/js/src/generated/types/baseOracleInitInfo.ts index bdb8e30d..12ff3dfd 100644 --- a/clients/js/src/generated/types/baseOracleInitInfo.ts +++ b/clients/js/src/generated/types/baseOracleInitInfo.ts @@ -36,7 +36,7 @@ import { export type BaseOracleInitInfo = { baseAddress: PublicKey; initPluginAuthority: Option; - lifecycleChecks: Option>; + lifecycleChecks: Array<[HookableLifecycleEvent, ExternalCheckResult]>; pda: Option; resultsOffset: Option; }; @@ -44,9 +44,7 @@ export type BaseOracleInitInfo = { export type BaseOracleInitInfoArgs = { baseAddress: PublicKey; initPluginAuthority: OptionOrNullable; - lifecycleChecks: OptionOrNullable< - Array<[HookableLifecycleEventArgs, ExternalCheckResultArgs]> - >; + lifecycleChecks: Array<[HookableLifecycleEventArgs, ExternalCheckResultArgs]>; pda: OptionOrNullable; resultsOffset: OptionOrNullable; }; @@ -61,13 +59,11 @@ export function getBaseOracleInitInfoSerializer(): Serializer< ['initPluginAuthority', option(getBasePluginAuthoritySerializer())], [ 'lifecycleChecks', - option( - array( - tuple([ - getHookableLifecycleEventSerializer(), - getExternalCheckResultSerializer(), - ]) - ) + array( + tuple([ + getHookableLifecycleEventSerializer(), + getExternalCheckResultSerializer(), + ]) ), ], ['pda', option(getBaseExtraAccountSerializer())], diff --git a/clients/rust/src/generated/types/oracle_init_info.rs b/clients/rust/src/generated/types/oracle_init_info.rs index f96c00e1..f6b73825 100644 --- a/clients/rust/src/generated/types/oracle_init_info.rs +++ b/clients/rust/src/generated/types/oracle_init_info.rs @@ -23,7 +23,7 @@ pub struct OracleInitInfo { )] pub base_address: Pubkey, pub init_plugin_authority: Option, - pub lifecycle_checks: Option>, + pub lifecycle_checks: Vec<(HookableLifecycleEvent, ExternalCheckResult)>, pub pda: Option, pub results_offset: Option, } diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 5d5e903b..6d5161df 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -2436,17 +2436,15 @@ { "name": "lifecycleChecks", "type": { - "option": { - "vec": { - "tuple": [ - { - "defined": "HookableLifecycleEvent" - }, - { - "defined": "ExternalCheckResult" - } - ] - } + "vec": { + "tuple": [ + { + "defined": "HookableLifecycleEvent" + }, + { + "defined": "ExternalCheckResult" + } + ] } } }, From 74abef4e0336ae56c55d65dab357bfa8dbd9a627 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 13:54:49 -0700 Subject: [PATCH 05/14] update oracle init helper --- clients/js/src/plugins/oracle.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/clients/js/src/plugins/oracle.ts b/clients/js/src/plugins/oracle.ts index 6bf223b2..a54ff14a 100644 --- a/clients/js/src/plugins/oracle.ts +++ b/clients/js/src/plugins/oracle.ts @@ -36,7 +36,7 @@ export type OracleInitInfoArgs = Omit< > & { type: 'Oracle'; initPluginAuthority?: PluginAuthority; - lifecycleChecks?: LifecycleChecks; + lifecycleChecks: LifecycleChecks; pda?: ExtraAccount; resultsOffset?: ValidationResultsOffset; }; @@ -57,9 +57,7 @@ export function oracleInitInfoArgsToBase( return { baseAddress: o.baseAddress, pda: o.pda ? extraAccountToBase(o.pda) : null, - lifecycleChecks: o.lifecycleChecks - ? lifecycleChecksToBase(o.lifecycleChecks) - : null, + lifecycleChecks: lifecycleChecksToBase(o.lifecycleChecks), initPluginAuthority: o.initPluginAuthority ? pluginAuthorityToBase(o.initPluginAuthority) : null, From ad785c19f2d08a82a18eb90108f9ebbf9f48918b Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 14:01:34 -0700 Subject: [PATCH 06/14] Fix Rust tests --- clients/rust/tests/add_external_plugins.rs | 4 ++-- clients/rust/tests/create_with_external_plugins.rs | 4 ++-- clients/rust/tests/remove_external_plugins.rs | 4 ++-- clients/rust/tests/update_external_plugins.rs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/clients/rust/tests/add_external_plugins.rs b/clients/rust/tests/add_external_plugins.rs index c3c8e5a9..7073714c 100644 --- a/clients/rust/tests/add_external_plugins.rs +++ b/clients/rust/tests/add_external_plugins.rs @@ -148,10 +148,10 @@ async fn test_add_oracle() { .init_info(ExternalPluginInitInfo::Oracle(OracleInitInfo { base_address: Pubkey::default(), init_plugin_authority: Some(PluginAuthority::UpdateAuthority), - lifecycle_checks: Some(vec![( + lifecycle_checks: vec![( HookableLifecycleEvent::Transfer, ExternalCheckResult { flags: 2 }, - )]), + )], pda: None, results_offset: None, })) diff --git a/clients/rust/tests/create_with_external_plugins.rs b/clients/rust/tests/create_with_external_plugins.rs index 44b98048..9bd19038 100644 --- a/clients/rust/tests/create_with_external_plugins.rs +++ b/clients/rust/tests/create_with_external_plugins.rs @@ -90,10 +90,10 @@ async fn test_create_oracle() { external_plugins: vec![ExternalPluginInitInfo::Oracle(OracleInitInfo { base_address: Pubkey::default(), init_plugin_authority: Some(PluginAuthority::UpdateAuthority), - lifecycle_checks: Some(vec![( + lifecycle_checks: vec![( HookableLifecycleEvent::Transfer, ExternalCheckResult { flags: 2 }, - )]), + )], pda: None, results_offset: None, })], diff --git a/clients/rust/tests/remove_external_plugins.rs b/clients/rust/tests/remove_external_plugins.rs index 85461cd0..3cda7788 100644 --- a/clients/rust/tests/remove_external_plugins.rs +++ b/clients/rust/tests/remove_external_plugins.rs @@ -125,10 +125,10 @@ async fn test_remove_oracle() { external_plugins: vec![ExternalPluginInitInfo::Oracle(OracleInitInfo { base_address: Pubkey::default(), init_plugin_authority: Some(PluginAuthority::UpdateAuthority), - lifecycle_checks: Some(vec![( + lifecycle_checks: vec![( HookableLifecycleEvent::Transfer, ExternalCheckResult { flags: 2 }, - )]), + )], pda: None, results_offset: None, })], diff --git a/clients/rust/tests/update_external_plugins.rs b/clients/rust/tests/update_external_plugins.rs index dd546743..04c77826 100644 --- a/clients/rust/tests/update_external_plugins.rs +++ b/clients/rust/tests/update_external_plugins.rs @@ -138,10 +138,10 @@ async fn test_update_oracle() { external_plugins: vec![ExternalPluginInitInfo::Oracle(OracleInitInfo { base_address: Pubkey::default(), init_plugin_authority: Some(PluginAuthority::UpdateAuthority), - lifecycle_checks: Some(vec![( + lifecycle_checks: vec![( HookableLifecycleEvent::Transfer, ExternalCheckResult { flags: 2 }, - )]), + )], pda: None, results_offset: None, })], From 1b951e86103f54ebb164c7c0bee295c98ba9a7a8 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 14:07:19 -0700 Subject: [PATCH 07/14] Require non-empty Vec --- programs/mpl-core/src/error.rs | 4 ++++ programs/mpl-core/src/plugins/utils.rs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index 642c63ce..c3089589 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -152,6 +152,10 @@ pub enum MplCoreError { /// 35 - Oracle external plugin can only be configured to deny #[error("Oracle external plugin can only be configured to deny")] OracleCanDenyOnly, + + /// 36 - Oracle external plugin must have at least one lifecycle check + #[error("Oracle external plugin must have at least one lifecycle check")] + OracleRequiresLifecycleCheck, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 89ad8c22..7ded10fb 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -331,7 +331,11 @@ pub fn initialize_external_plugin<'a, T: DataBlob + SolanaAccount>( init_info.lifecycle_checks.clone(), ), ExternalPluginInitInfo::Oracle(init_info) => { - // You cannot configure an Oracle plugin to approve lifecycle events. + if init_info.lifecycle_checks.is_empty() { + return Err(MplCoreError::OracleRequiresLifecycleCheck.into()); + } + + // Oracle can only deny lifecycle events. for (_, result) in &init_info.lifecycle_checks { // Deny is bit 2. if result.flags != 0x2 { From d3c799f9aeb3e0ff101cb3fb0086ef1f28615c2c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 14:08:09 -0700 Subject: [PATCH 08/14] Regenerate IDL and clients --- clients/js/src/generated/errors/mplCore.ts | 20 +++++++++++++++++++ clients/rust/src/generated/errors/mpl_core.rs | 3 +++ idls/mpl_core.json | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 1c22df20..5e5f2a71 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -509,6 +509,26 @@ export class OracleCanDenyOnlyError extends ProgramError { codeToErrorMap.set(0x23, OracleCanDenyOnlyError); nameToErrorMap.set('OracleCanDenyOnly', OracleCanDenyOnlyError); +/** OracleRequiresLifecycleCheck: Oracle external plugin must have at least one lifecycle check */ +export class OracleRequiresLifecycleCheckError extends ProgramError { + override readonly name: string = 'OracleRequiresLifecycleCheck'; + + readonly code: number = 0x24; // 36 + + constructor(program: Program, cause?: Error) { + super( + 'Oracle external plugin must have at least one lifecycle check', + program, + cause + ); + } +} +codeToErrorMap.set(0x24, OracleRequiresLifecycleCheckError); +nameToErrorMap.set( + 'OracleRequiresLifecycleCheck', + OracleRequiresLifecycleCheckError +); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 07d39ad3..b4381de9 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -118,6 +118,9 @@ pub enum MplCoreError { /// 35 (0x23) - Oracle external plugin can only be configured to deny #[error("Oracle external plugin can only be configured to deny")] OracleCanDenyOnly, + /// 36 (0x24) - Oracle external plugin must have at least one lifecycle check + #[error("Oracle external plugin must have at least one lifecycle check")] + OracleRequiresLifecycleCheck, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 6d5161df..3461799c 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -4166,6 +4166,11 @@ "code": 35, "name": "OracleCanDenyOnly", "msg": "Oracle external plugin can only be configured to deny" + }, + { + "code": 36, + "name": "OracleRequiresLifecycleCheck", + "msg": "Oracle external plugin must have at least one lifecycle check" } ], "metadata": { From 7751abcf154362e3107a12258d24fd82dda892b5 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 14:29:13 -0700 Subject: [PATCH 09/14] Change CanDeny to CanReject --- programs/mpl-core/src/error.rs | 6 +++--- programs/mpl-core/src/plugins/utils.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index c3089589..31e6f997 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -149,9 +149,9 @@ pub enum MplCoreError { #[error("Missing account needed for external plugin")] MissingExternalAccount, - /// 35 - Oracle external plugin can only be configured to deny - #[error("Oracle external plugin can only be configured to deny")] - OracleCanDenyOnly, + /// 35 - Oracle external plugin can only be configured to reject + #[error("Oracle external plugin can only be configured to reject")] + OracleCanRejectOnly, /// 36 - Oracle external plugin must have at least one lifecycle check #[error("Oracle external plugin must have at least one lifecycle check")] diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 7ded10fb..b6017f04 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -339,7 +339,7 @@ pub fn initialize_external_plugin<'a, T: DataBlob + SolanaAccount>( for (_, result) in &init_info.lifecycle_checks { // Deny is bit 2. if result.flags != 0x2 { - return Err(MplCoreError::OracleCanDenyOnly.into()); + return Err(MplCoreError::OracleCanRejectOnly.into()); } } From 4c65bf79f8f41081accfa4c8819664d34e32f10c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 2 May 2024 14:30:03 -0700 Subject: [PATCH 10/14] Regenerate IDL and clients --- clients/js/src/generated/errors/mplCore.ts | 12 ++++++------ clients/rust/src/generated/errors/mpl_core.rs | 6 +++--- idls/mpl_core.json | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 5e5f2a71..00582faf 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -492,22 +492,22 @@ export class MissingExternalAccountError extends ProgramError { codeToErrorMap.set(0x22, MissingExternalAccountError); nameToErrorMap.set('MissingExternalAccount', MissingExternalAccountError); -/** OracleCanDenyOnly: Oracle external plugin can only be configured to deny */ -export class OracleCanDenyOnlyError extends ProgramError { - override readonly name: string = 'OracleCanDenyOnly'; +/** OracleCanRejectOnly: Oracle external plugin can only be configured to reject */ +export class OracleCanRejectOnlyError extends ProgramError { + override readonly name: string = 'OracleCanRejectOnly'; readonly code: number = 0x23; // 35 constructor(program: Program, cause?: Error) { super( - 'Oracle external plugin can only be configured to deny', + 'Oracle external plugin can only be configured to reject', program, cause ); } } -codeToErrorMap.set(0x23, OracleCanDenyOnlyError); -nameToErrorMap.set('OracleCanDenyOnly', OracleCanDenyOnlyError); +codeToErrorMap.set(0x23, OracleCanRejectOnlyError); +nameToErrorMap.set('OracleCanRejectOnly', OracleCanRejectOnlyError); /** OracleRequiresLifecycleCheck: Oracle external plugin must have at least one lifecycle check */ export class OracleRequiresLifecycleCheckError extends ProgramError { diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index b4381de9..f73d53f6 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -115,9 +115,9 @@ pub enum MplCoreError { /// 34 (0x22) - Missing account needed for external plugin #[error("Missing account needed for external plugin")] MissingExternalAccount, - /// 35 (0x23) - Oracle external plugin can only be configured to deny - #[error("Oracle external plugin can only be configured to deny")] - OracleCanDenyOnly, + /// 35 (0x23) - Oracle external plugin can only be configured to reject + #[error("Oracle external plugin can only be configured to reject")] + OracleCanRejectOnly, /// 36 (0x24) - Oracle external plugin must have at least one lifecycle check #[error("Oracle external plugin must have at least one lifecycle check")] OracleRequiresLifecycleCheck, diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 3461799c..e0dc3e56 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -4164,8 +4164,8 @@ }, { "code": 35, - "name": "OracleCanDenyOnly", - "msg": "Oracle external plugin can only be configured to deny" + "name": "OracleCanRejectOnly", + "msg": "Oracle external plugin can only be configured to reject" }, { "code": 36, From 6d293afa8d54dffea9573fb75c18aebea0d7d5ef Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 3 May 2024 00:04:32 -0700 Subject: [PATCH 11/14] Format fix --- clients/js/src/helpers/lifecycle.ts | 2 +- clients/js/src/plugins/lifecycleChecks.ts | 2 +- clients/js/test/helps/lifecycle.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/js/src/helpers/lifecycle.ts b/clients/js/src/helpers/lifecycle.ts index f5dc93dd..c35e74da 100644 --- a/clients/js/src/helpers/lifecycle.ts +++ b/clients/js/src/helpers/lifecycle.ts @@ -96,7 +96,7 @@ export async function validateTransfer( } if (isFrozen(asset, collection)) { - return LifecycleValidationError.AssetFrozen + return LifecycleValidationError.AssetFrozen; } if (dAsset.oracles?.length) { diff --git a/clients/js/src/plugins/lifecycleChecks.ts b/clients/js/src/plugins/lifecycleChecks.ts index d1b5acdb..6aedfaf8 100644 --- a/clients/js/src/plugins/lifecycleChecks.ts +++ b/clients/js/src/plugins/lifecycleChecks.ts @@ -85,7 +85,7 @@ export function lifecycleChecksToBase( }) .filter((x) => x !== null) as [ HookableLifecycleEvent, - ExternalCheckResult + ExternalCheckResult, ][]; } diff --git a/clients/js/test/helps/lifecycle.test.ts b/clients/js/test/helps/lifecycle.test.ts index cc956bd2..c98f58a4 100644 --- a/clients/js/test/helps/lifecycle.test.ts +++ b/clients/js/test/helps/lifecycle.test.ts @@ -218,7 +218,7 @@ test('it can detect non burnable from frozen asset', async (t) => { t.assert(!canBurn(owner.publicKey, asset)); t.is( await validateBurn(umi, { authority: owner.publicKey, asset }), - LifecycleValidationError.AssetFrozen + LifecycleValidationError.AssetFrozen ); }); From 6dff5b0d2858b021e3ab39e29d20ab7c5043c18c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 3 May 2024 00:17:08 -0700 Subject: [PATCH 12/14] Add test --- .../js/test/externalPlugins/oracle.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index efdada63..6ca1d24c 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -261,6 +261,45 @@ test('it can use fixed address oracle to deny transfer', async (t) => { }); }); +test('it cannot configure oracle with no lifecycle checks', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + const owner = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Approved, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // Validate cannot have Oracle with no `lifecycleChecks`. + const result = createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: {}, + baseAddress: account.publicKey, + }, + ], + }); + + await t.throwsAsync(result, { name: 'OracleRequiresLifecycleCheck' }); +}); + test('it cannot configure oracle to approve', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); From 4fe27a8d9bf6f7a0c91b353497ac04c68ca0b4bb Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 3 May 2024 00:42:22 -0700 Subject: [PATCH 13/14] Add more tests --- .../js/test/externalPlugins/oracle.test.ts | 160 +++++++++++++----- 1 file changed, 114 insertions(+), 46 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 6ca1d24c..a27bce00 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -39,6 +39,7 @@ import { OracleInitInfoArgs, transfer, update, + addPlugin, } from '../../src'; const createUmi = async () => @@ -261,28 +262,12 @@ test('it can use fixed address oracle to deny transfer', async (t) => { }); }); -test('it cannot configure oracle with no lifecycle checks', async (t) => { +test('it cannot create asset with oracle with no lifecycle checks', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); - // write to example program oracle account - await fixedAccountInit(umi, { - account, - signer: umi.identity, - payer: umi.identity, - args: { - oracleData: { - __kind: 'V1', - create: ExternalValidationResult.Pass, - update: ExternalValidationResult.Pass, - transfer: ExternalValidationResult.Approved, - burn: ExternalValidationResult.Pass, - }, - }, - }).sendAndConfirm(umi); - - // Validate cannot have Oracle with no `lifecycleChecks`. + // Oracle with no lifecycle checks const result = createAsset(umi, { owner, plugins: [ @@ -300,28 +285,49 @@ test('it cannot configure oracle with no lifecycle checks', async (t) => { await t.throwsAsync(result, { name: 'OracleRequiresLifecycleCheck' }); }); -test('it cannot configure oracle to approve', async (t) => { +test('it cannot add oracle with no lifecycle checks to asset', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); - // write to example program oracle account - await fixedAccountInit(umi, { - account, - signer: umi.identity, - payer: umi.identity, - args: { - oracleData: { - __kind: 'V1', - create: ExternalValidationResult.Pass, - update: ExternalValidationResult.Pass, - transfer: ExternalValidationResult.Approved, - burn: ExternalValidationResult.Pass, + const asset = await createAsset(umi, { + owner, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); + + // Oracle with no lifecycle checks + const result = addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', }, + lifecycleChecks: {}, + baseAddress: account.publicKey, }, }).sendAndConfirm(umi); - // Validate cannot have Oracle with `CheckResult.CAN_APPROVE` + await t.throwsAsync(result, { name: 'OracleRequiresLifecycleCheck' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); +}); + +test('it cannot create asset with oracle that can approve', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + const owner = generateSigner(umi); + + // Oracle with `CheckResult.CAN_APPROVE` const result = createAsset(umi, { owner, plugins: [ @@ -341,28 +347,51 @@ test('it cannot configure oracle to approve', async (t) => { await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); }); -test('it cannot configure oracle to listen', async (t) => { +test('it cannot add oracle that can approve to asset', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); - // write to example program oracle account - await fixedAccountInit(umi, { - account, - signer: umi.identity, - payer: umi.identity, - args: { - oracleData: { - __kind: 'V1', - create: ExternalValidationResult.Pass, - update: ExternalValidationResult.Pass, - transfer: ExternalValidationResult.Approved, - burn: ExternalValidationResult.Pass, + const asset = await createAsset(umi, { + owner, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); + + // Oracle with `CheckResult.CAN_APPROVE` + const result = addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_APPROVE], }, + baseAddress: account.publicKey, }, }).sendAndConfirm(umi); - // Validate cannot have Oracle with `CheckResult.CAN_LISTEN` + await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); +}); + +test('it cannot create asset with oracle that can listen', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + const owner = generateSigner(umi); + + // Oracle with `CheckResult.CAN_LISTEN` const result = createAsset(umi, { owner, plugins: [ @@ -382,6 +411,45 @@ test('it cannot configure oracle to listen', async (t) => { await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); }); +test('it cannot add oracle that can listen to asset', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + const owner = generateSigner(umi); + + const asset = await createAsset(umi, { + owner, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); + + // Oracle with `CheckResult.CAN_LISTEN` + const result = addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_LISTEN], + }, + baseAddress: account.publicKey, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); +}); + test('it cannot use fixed address oracle to deny transfer if not registered for lifecycle event', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); From 39c44fa282a3008a08f97985d7e0a493bb0d63a6 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Fri, 3 May 2024 01:01:34 -0700 Subject: [PATCH 14/14] More tests for invalid checks and multiple oracle test --- .../js/test/externalPlugins/oracle.test.ts | 168 +++++++++++++++++- 1 file changed, 165 insertions(+), 3 deletions(-) diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index a27bce00..77875da7 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -262,7 +262,7 @@ test('it can use fixed address oracle to deny transfer', async (t) => { }); }); -test('it cannot create asset with oracle with no lifecycle checks', async (t) => { +test('it cannot create asset with oracle that has no lifecycle checks', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); @@ -347,7 +347,32 @@ test('it cannot create asset with oracle that can approve', async (t) => { await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); }); -test('it cannot add oracle that can approve to asset', async (t) => { +test('it cannot create asset with oracle that can approve in addition to reject', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + const owner = generateSigner(umi); + + // Oracle with `CheckResult.CAN_APPROVE` + const result = createAsset(umi, { + owner, + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_APPROVE, CheckResult.CAN_REJECT], + }, + baseAddress: account.publicKey, + }, + ], + }); + + await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); +}); + +test('it cannot add oracle to asset that can approve', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); @@ -386,6 +411,45 @@ test('it cannot add oracle that can approve to asset', async (t) => { }); }); +test('it cannot add oracle to asset that can approve in addition to reject', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + const owner = generateSigner(umi); + + const asset = await createAsset(umi, { + owner, + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); + + // Oracle with `CheckResult.CAN_APPROVE` + const result = addPlugin(umi, { + asset: asset.publicKey, + plugin: { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_APPROVE, CheckResult.CAN_REJECT], + }, + baseAddress: account.publicKey, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: owner.publicKey, + }); +}); + test('it cannot create asset with oracle that can listen', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); @@ -411,7 +475,7 @@ test('it cannot create asset with oracle that can listen', async (t) => { await t.throwsAsync(result, { name: 'OracleCanRejectOnly' }); }); -test('it cannot add oracle that can listen to asset', async (t) => { +test('it cannot add oracle to asset that can listen', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); const owner = generateSigner(umi); @@ -1402,3 +1466,101 @@ test('it can use preconfigured asset pda custom offset oracle to deny update', a name: 'new name 2', }); }); + +test('it can use one fixed address oracle to deny transfer when a second oracle allows it', async (t) => { + const umi = await createUmi(); + const account1 = generateSigner(umi); + const account2 = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: account1, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Rejected, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account: account2, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + // create asset referencing both oracle accounts + const asset = await createAsset(umi, { + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: account1.publicKey, + }, + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: account2.publicKey, + }, + ], + }); + + const newOwner = generateSigner(umi); + + const result = transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await fixedAccountSet(umi, { + account: account1.publicKey, + signer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await transfer(umi, { + asset, + newOwner: newOwner.publicKey, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: newOwner.publicKey, + }); +});