diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index f92d8afc..49cbaa0d 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -21,7 +21,11 @@ import { preconfiguredAssetPdaCustomOffsetSet, close, } from '@metaplex-foundation/mpl-core-oracle-example'; -import { generateSigner, sol } from '@metaplex-foundation/umi'; +import { + generateSigner, + sol, + assertAccountExists, +} from '@metaplex-foundation/umi'; import { generateSignerWithSol } from '@metaplex-foundation/umi-bundle-tests'; import { createAccount } from '@metaplex-foundation/mpl-toolbox'; import { @@ -37,6 +41,8 @@ import { burn, CheckResult, create, + createCollection, + updateCollectionPlugin, findOracleAccount, OracleInitInfoArgs, transfer, @@ -266,7 +272,7 @@ test('it can add multiple oracles and internal plugins to asset', async (t) => { }); }); -test.skip('add oracle to asset with no offset', async (t) => { +test('add oracle to asset with no offset', async (t) => { const umi = await createUmi(); const account = generateSigner(umi); @@ -2734,7 +2740,7 @@ test('it can update asset to different size name with oracle', async (t) => { }); }); -test('it can update oracle to different size external plugin adapter', async (t) => { +test('it can update oracle to smaller registry record', async (t) => { const umi = await createUmi(); const oracleSigner = generateSigner(umi); await fixedAccountInit(umi, { @@ -2763,8 +2769,6 @@ test('it can update oracle to different size external plugin adapter', async (t) type: 'Anchor', }, lifecycleChecks: { - create: [CheckResult.CAN_REJECT], - update: [CheckResult.CAN_REJECT], transfer: [CheckResult.CAN_REJECT], burn: [CheckResult.CAN_REJECT], }, @@ -2788,8 +2792,6 @@ test('it can update oracle to different size external plugin adapter', async (t) type: 'Anchor', }, lifecycleChecks: { - create: [CheckResult.CAN_REJECT], - update: [CheckResult.CAN_REJECT], transfer: [CheckResult.CAN_REJECT], burn: [CheckResult.CAN_REJECT], }, @@ -2799,9 +2801,128 @@ test('it can update oracle to different size external plugin adapter', async (t) ], }); + const accountBefore = await umi.rpc.getAccount(asset.publicKey); + t.true(accountBefore.exists); + assertAccountExists(accountBefore); + const beforeLength = accountBefore.data.length; + await updatePlugin(umi, { asset: asset.publicKey, + plugin: { + key: { + type: 'Oracle', + baseAddress: oracleSigner.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + uri: 'https://example.com', + name: 'Test name', + owner: umi.identity.publicKey, + asset: asset.publicKey, + oracles: [ + { + authority: { + type: 'UpdateAuthority', + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: undefined, + update: undefined, + transfer: [CheckResult.CAN_REJECT], + burn: undefined, + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + + // Validate that account got smaller by specific amount. + const accountAfter = await umi.rpc.getAccount(asset.publicKey); + t.true(accountAfter.exists); + assertAccountExists(accountAfter); + const afterLength = accountAfter.data.length; + t.is(afterLength - beforeLength, -5); +}); + +test('it can update oracle to larger registry record', async (t) => { + const umi = await createUmi(); + const oracleSigner = generateSigner(umi); + await fixedAccountInit(umi, { + signer: umi.identity, + account: oracleSigner, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + update: ExternalValidationResult.Rejected, + }, + }, + }).sendAndConfirm(umi); + + const asset = generateSigner(umi); + await create(umi, { + asset, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + }, + ], + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + uri: 'https://example.com', + name: 'Test name', + owner: umi.identity.publicKey, + asset: asset.publicKey, + oracles: [ + { + authority: { + type: 'UpdateAuthority', + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + const accountBefore = await umi.rpc.getAccount(asset.publicKey); + t.true(accountBefore.exists); + assertAccountExists(accountBefore); + const beforeLength = accountBefore.data.length; + + await updatePlugin(umi, { + asset: asset.publicKey, plugin: { key: { type: 'Oracle', @@ -2812,7 +2933,10 @@ test('it can update oracle to different size external plugin adapter', async (t) type: 'Anchor', }, lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + update: [CheckResult.CAN_REJECT], transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], }, }, }).sendAndConfirm(umi); @@ -2832,13 +2956,23 @@ test('it can update oracle to different size external plugin adapter', async (t) type: 'Anchor', }, lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + update: [CheckResult.CAN_REJECT], transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], }, baseAddress: oracleSigner.publicKey, baseAddressConfig: undefined, }, ], }); + + // Validate that account got larger by specific amount. + const accountAfter = await umi.rpc.getAccount(asset.publicKey); + t.true(accountAfter.exists); + assertAccountExists(accountAfter); + const afterLength = accountAfter.data.length; + t.is(afterLength - beforeLength, 15); }); test('it create fails but does not panic when oracle account does not exist', async (t) => { @@ -3035,3 +3169,449 @@ test('it empty account does not default to valid oracle', async (t) => { await t.throwsAsync(result, { name: 'UninitializedOracleAccount' }); }); + +test('it can update oracle with external plugin authority different than asset update authority', async (t) => { + const umi = await createUmi(); + const oracleSigner = generateSigner(umi); + await fixedAccountInit(umi, { + signer: umi.identity, + account: oracleSigner, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = generateSigner(umi); + const oracleUpdateAuthority = generateSigner(umi); + await create(umi, { + asset, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + initPluginAuthority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + }, + ], + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + uri: 'https://example.com', + name: 'Test name', + owner: umi.identity.publicKey, + asset: asset.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + + await updatePlugin(umi, { + asset: asset.publicKey, + authority: oracleUpdateAuthority, + plugin: { + key: { + type: 'Oracle', + baseAddress: oracleSigner.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], + }, + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + uri: 'https://example.com', + name: 'Test name', + owner: umi.identity.publicKey, + asset: asset.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); +}); + +test('it cannot update oracle using update authority when different from external plugin authority', async (t) => { + const umi = await createUmi(); + const oracleSigner = generateSigner(umi); + await fixedAccountInit(umi, { + signer: umi.identity, + account: oracleSigner, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const asset = generateSigner(umi); + const oracleUpdateAuthority = generateSigner(umi); + await create(umi, { + asset, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + initPluginAuthority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + }, + ], + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + uri: 'https://example.com', + name: 'Test name', + owner: umi.identity.publicKey, + asset: asset.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + + const result = updatePlugin(umi, { + asset: asset.publicKey, + authority: umi.identity, + plugin: { + key: { + type: 'Oracle', + baseAddress: oracleSigner.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], + }, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await assertAsset(t, umi, { + uri: 'https://example.com', + name: 'Test name', + owner: umi.identity.publicKey, + asset: asset.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); +}); + +test('it can update oracle on collection with external plugin authority different than asset update authority', async (t) => { + const umi = await createUmi(); + const oracleSigner = generateSigner(umi); + await fixedAccountInit(umi, { + signer: umi.identity, + account: oracleSigner, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const collection = generateSigner(umi); + const oracleUpdateAuthority = generateSigner(umi); + await createCollection(umi, { + collection, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + initPluginAuthority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + }, + ], + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + uri: 'https://example.com', + name: 'Test name', + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + + await updateCollectionPlugin(umi, { + collection: collection.publicKey, + authority: oracleUpdateAuthority, + plugin: { + key: { + type: 'Oracle', + baseAddress: oracleSigner.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], + }, + }, + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + uri: 'https://example.com', + name: 'Test name', + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); +}); + +test('it cannot update oracle on collection using update authority when different from external plugin authority', async (t) => { + const umi = await createUmi(); + const oracleSigner = generateSigner(umi); + await fixedAccountInit(umi, { + signer: umi.identity, + account: oracleSigner, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const collection = generateSigner(umi); + const oracleUpdateAuthority = generateSigner(umi); + await createCollection(umi, { + collection, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + initPluginAuthority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + }, + ], + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + uri: 'https://example.com', + name: 'Test name', + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + + const result = updateCollectionPlugin(umi, { + collection: collection.publicKey, + authority: umi.identity, + plugin: { + key: { + type: 'Oracle', + baseAddress: oracleSigner.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'NoOffset', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + burn: [CheckResult.CAN_REJECT], + }, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + + await assertCollection(t, umi, { + uri: 'https://example.com', + name: 'Test name', + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + oracles: [ + { + authority: { + type: 'Address', + address: oracleUpdateAuthority.publicKey, + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + transfer: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); +}); diff --git a/programs/mpl-core/src/plugins/external_plugin_adapters.rs b/programs/mpl-core/src/plugins/external_plugin_adapters.rs index ceb25fb4..d4ba7828 100644 --- a/programs/mpl-core/src/plugins/external_plugin_adapters.rs +++ b/programs/mpl-core/src/plugins/external_plugin_adapters.rs @@ -7,6 +7,7 @@ use strum::EnumCount; use crate::{ error::MplCoreError, + plugins::lifecycle::{approve, reject}, state::{AssetV1, SolanaAccount}, }; @@ -198,6 +199,53 @@ impl ExternalPluginAdapter { } } + /// Validate the add external plugin adapter lifecycle event. + pub(crate) fn validate_update_external_plugin_adapter( + external_plugin_adapter: &ExternalPluginAdapter, + ctx: &PluginValidationContext, + ) -> Result { + let resolved_authorities = ctx + .resolved_authorities + .ok_or(MplCoreError::InvalidAuthority)?; + let base_result = if resolved_authorities.contains(ctx.self_authority) { + solana_program::msg!("Base: Approved"); + ValidationResult::Approved + } else { + ValidationResult::Pass + }; + + let result = match external_plugin_adapter { + ExternalPluginAdapter::LifecycleHook(lifecycle_hook) => { + lifecycle_hook.validate_update_external_plugin_adapter(ctx) + } + ExternalPluginAdapter::Oracle(oracle) => { + oracle.validate_update_external_plugin_adapter(ctx) + } + ExternalPluginAdapter::DataStore(app_data) => { + app_data.validate_update_external_plugin_adapter(ctx) + } + }?; + + match (&base_result, &result) { + (ValidationResult::Approved, ValidationResult::Approved) => { + approve!() + } + (ValidationResult::Approved, ValidationResult::Rejected) => { + reject!() + } + (ValidationResult::Rejected, ValidationResult::Approved) => { + reject!() + } + (ValidationResult::Rejected, ValidationResult::Rejected) => { + reject!() + } + (ValidationResult::Pass, _) => Ok(result), + (ValidationResult::ForceApproved, _) => unreachable!(), + (_, ValidationResult::Pass) => Ok(base_result), + (_, ValidationResult::ForceApproved) => unreachable!(), + } + } + /// Load and deserialize a plugin from an offset in the account. pub fn load(account: &AccountInfo, offset: usize) -> Result { let mut bytes: &[u8] = &(*account.data).borrow()[offset..]; diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 84998626..119fdf11 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -733,80 +733,6 @@ impl Plugin { Plugin::Autograph(autograph) => autograph.validate_remove_external_plugin_adapter(ctx), } } - - /// Route the validation of the update_plugin action to the appropriate plugin. - /// There is no check for updating a plugin because the plugin itself MUST validate the change. - pub(crate) fn validate_update_external_plugin_adapter( - plugin: &Plugin, - ctx: &PluginValidationContext, - ) -> Result { - let resolved_authorities = ctx - .resolved_authorities - .ok_or(MplCoreError::InvalidAuthority)?; - let base_result = if resolved_authorities.contains(ctx.self_authority) { - solana_program::msg!("Base: Approved"); - ValidationResult::Approved - } else { - ValidationResult::Pass - }; - - let result = match plugin { - Plugin::Royalties(royalties) => royalties.validate_update_external_plugin_adapter(ctx), - Plugin::FreezeDelegate(freeze) => freeze.validate_update_external_plugin_adapter(ctx), - Plugin::BurnDelegate(burn) => burn.validate_update_external_plugin_adapter(ctx), - Plugin::TransferDelegate(transfer) => { - transfer.validate_update_external_plugin_adapter(ctx) - } - Plugin::UpdateDelegate(update_delegate) => { - update_delegate.validate_update_external_plugin_adapter(ctx) - } - Plugin::PermanentFreezeDelegate(permanent_freeze) => { - permanent_freeze.validate_update_external_plugin_adapter(ctx) - } - Plugin::Attributes(attributes) => { - attributes.validate_update_external_plugin_adapter(ctx) - } - Plugin::PermanentTransferDelegate(permanent_transfer) => { - permanent_transfer.validate_update_external_plugin_adapter(ctx) - } - Plugin::PermanentBurnDelegate(permanent_burn) => { - permanent_burn.validate_update_external_plugin_adapter(ctx) - } - Plugin::Edition(edition) => edition.validate_update_external_plugin_adapter(ctx), - Plugin::MasterEdition(master_edition) => { - master_edition.validate_update_external_plugin_adapter(ctx) - } - Plugin::AddBlocker(add_blocker) => { - add_blocker.validate_update_external_plugin_adapter(ctx) - } - Plugin::ImmutableMetadata(immutable_metadata) => { - immutable_metadata.validate_update_external_plugin_adapter(ctx) - } - Plugin::VerifiedCreators(verified_creators) => { - verified_creators.validate_update_external_plugin_adapter(ctx) - } - Plugin::Autograph(autograph) => autograph.validate_update_external_plugin_adapter(ctx), - }?; - - match (&base_result, &result) { - (ValidationResult::Approved, ValidationResult::Approved) => { - approve!() - } - (ValidationResult::Approved, ValidationResult::Rejected) => { - reject!() - } - (ValidationResult::Rejected, ValidationResult::Approved) => { - reject!() - } - (ValidationResult::Rejected, ValidationResult::Rejected) => { - reject!() - } - (ValidationResult::Pass, _) => Ok(result), - (ValidationResult::ForceApproved, _) => force_approve!(), - (_, ValidationResult::Pass) => Ok(base_result), - (_, ValidationResult::ForceApproved) => force_approve!(), - } - } } /// Lifecycle validations diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index a0dd8543..117cd7a6 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -683,42 +683,67 @@ pub(crate) fn find_external_plugin_adapter<'b>( plugin_key: &ExternalPluginAdapterKey, account: &AccountInfo<'_>, ) -> Result<(Option, Option<&'b ExternalRegistryRecord>), ProgramError> { - let mut result = (None, None); for (i, record) in plugin_registry.external_registry.iter().enumerate() { - if record.plugin_type == ExternalPluginAdapterType::from(plugin_key) - && (match plugin_key { - ExternalPluginAdapterKey::LifecycleHook(address) - | ExternalPluginAdapterKey::Oracle(address) => { - let pubkey_offset = record - .offset - .checked_add(1) - .ok_or(MplCoreError::NumericalOverflow)?; - address - == &match Pubkey::deserialize(&mut &account.data.borrow()[pubkey_offset..]) - { - Ok(address) => address, - Err(_) => return Err(MplCoreError::DeserializationError.into()), - } - } - ExternalPluginAdapterKey::DataStore(authority) => { - let authority_offset = record - .offset - .checked_add(1) - .ok_or(MplCoreError::NumericalOverflow)?; - authority - == &match Authority::deserialize( - &mut &account.data.borrow()[authority_offset..], - ) { - Ok(authority) => authority, - Err(_) => return Err(MplCoreError::DeserializationError.into()), - } - } - }) - { - result = (Some(i), Some(record)); - break; + if check_plugin_key(record, plugin_key, account)? { + return Ok((Some(i), Some(record))); + } + } + + Ok((None, None)) +} + +pub(crate) fn find_external_plugin_adapter_mut<'b>( + plugin_registry: &'b mut PluginRegistryV1, + plugin_key: &ExternalPluginAdapterKey, + account: &AccountInfo<'_>, +) -> Result<(Option, Option<&'b mut ExternalRegistryRecord>), ProgramError> { + for (i, record) in plugin_registry.external_registry.iter_mut().enumerate() { + let record_ref = &*record; + + if check_plugin_key(record_ref, plugin_key, account)? { + return Ok((Some(i), Some(record))); } } - Ok(result) + Ok((None, None)) +} + +fn check_plugin_key( + record_ref: &ExternalRegistryRecord, + plugin_key: &ExternalPluginAdapterKey, + account: &AccountInfo, +) -> Result { + if record_ref.plugin_type == ExternalPluginAdapterType::from(plugin_key) + && (match plugin_key { + ExternalPluginAdapterKey::LifecycleHook(address) + | ExternalPluginAdapterKey::Oracle(address) => { + let pubkey_offset = record_ref + .offset + .checked_add(1) + .ok_or(MplCoreError::NumericalOverflow)?; + address + == &match Pubkey::deserialize(&mut &account.data.borrow()[pubkey_offset..]) { + Ok(address) => address, + Err(_) => return Err(MplCoreError::DeserializationError.into()), + } + } + ExternalPluginAdapterKey::DataStore(authority) => { + let authority_offset = record_ref + .offset + .checked_add(1) + .ok_or(MplCoreError::NumericalOverflow)?; + authority + == &match Authority::deserialize( + &mut &account.data.borrow()[authority_offset..], + ) { + Ok(authority) => authority, + Err(_) => return Err(MplCoreError::DeserializationError.into()), + } + } + }) + { + Ok(true) + } else { + Ok(false) + } } diff --git a/programs/mpl-core/src/processor/update_external_plugin_adapter.rs b/programs/mpl-core/src/processor/update_external_plugin_adapter.rs index 3cafc18f..0e030b43 100644 --- a/programs/mpl-core/src/processor/update_external_plugin_adapter.rs +++ b/programs/mpl-core/src/processor/update_external_plugin_adapter.rs @@ -10,14 +10,14 @@ use crate::{ UpdateCollectionExternalPluginAdapterV1Accounts, UpdateExternalPluginAdapterV1Accounts, }, plugins::{ - fetch_wrapped_external_plugin_adapter, find_external_plugin_adapter, ExternalPluginAdapter, - ExternalPluginAdapterKey, ExternalPluginAdapterUpdateInfo, Plugin, PluginHeaderV1, - PluginRegistryV1, PluginType, + fetch_wrapped_external_plugin_adapter, find_external_plugin_adapter_mut, + ExternalPluginAdapter, ExternalPluginAdapterKey, ExternalPluginAdapterUpdateInfo, + PluginHeaderV1, PluginRegistryV1, PluginValidationContext, ValidationResult, }, state::{AssetV1, CollectionV1, DataBlob, Key, SolanaAccount}, utils::{ - load_key, resize_or_reallocate_account, resolve_authority, validate_asset_permissions, - validate_collection_permissions, + fetch_core_data, load_key, resize_or_reallocate_account, resolve_authority, + resolve_pubkey_to_authorities, resolve_pubkey_to_authorities_collection, }, }; @@ -56,33 +56,38 @@ pub(crate) fn update_external_plugin_adapter<'a>( return Err(MplCoreError::NotAvailable.into()); } - let (_, plugin) = + let (mut asset, plugin_header, plugin_registry) = + fetch_core_data::(ctx.accounts.asset)?; + let resolved_authorities = + resolve_pubkey_to_authorities(authority, ctx.accounts.collection, &asset)?; + let (external_registry_record_authority, external_plugin_adapter) = fetch_wrapped_external_plugin_adapter::(ctx.accounts.asset, None, &args.key)?; - let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( + let validation_ctx = PluginValidationContext { accounts, - authority, - ctx.accounts.asset, - ctx.accounts.collection, - None, - None, - Some(&plugin), - AssetV1::check_update_external_plugin_adapter, - CollectionV1::check_update_external_plugin_adapter, - PluginType::check_update_external_plugin_adapter, - AssetV1::validate_update_external_plugin_adapter, - CollectionV1::validate_update_external_plugin_adapter, - Plugin::validate_update_external_plugin_adapter, - None, - None, - )?; + asset_info: Some(ctx.accounts.asset), + collection_info: ctx.accounts.collection, + self_authority: &external_registry_record_authority, + authority_info: authority, + resolved_authorities: Some(&resolved_authorities), + new_owner: None, + target_plugin: None, + }; + + if ExternalPluginAdapter::validate_update_external_plugin_adapter( + &external_plugin_adapter, + &validation_ctx, + )? != ValidationResult::Approved + { + return Err(MplCoreError::InvalidAuthority.into()); + } // Increment sequence number and save only if it is `Some(_)`. asset.increment_seq_and_save(ctx.accounts.asset)?; process_update_external_plugin_adapter( asset, - plugin, + external_plugin_adapter, args.key, args.update_info, plugin_header, @@ -123,30 +128,39 @@ pub(crate) fn update_collection_external_plugin_adapter<'a>( } } - let (_, plugin) = fetch_wrapped_external_plugin_adapter::( - ctx.accounts.collection, - None, - &args.key, - )?; - - // Validate collection permissions. - let (collection, plugin_header, plugin_registry) = validate_collection_permissions( + let (collection, plugin_header, plugin_registry) = + fetch_core_data::(ctx.accounts.collection)?; + let resolved_authorities = + resolve_pubkey_to_authorities_collection(authority, ctx.accounts.collection)?; + let (external_registry_record_authority, external_plugin_adapter) = + fetch_wrapped_external_plugin_adapter::( + ctx.accounts.collection, + None, + &args.key, + )?; + + let validation_ctx = PluginValidationContext { accounts, - authority, - ctx.accounts.collection, - None, - Some(&plugin), - CollectionV1::check_update_external_plugin_adapter, - PluginType::check_update_external_plugin_adapter, - CollectionV1::validate_update_external_plugin_adapter, - Plugin::validate_update_external_plugin_adapter, - None, - None, - )?; + asset_info: None, + collection_info: Some(ctx.accounts.collection), + self_authority: &external_registry_record_authority, + authority_info: authority, + resolved_authorities: Some(&resolved_authorities), + new_owner: None, + target_plugin: None, + }; + + if ExternalPluginAdapter::validate_update_external_plugin_adapter( + &external_plugin_adapter, + &validation_ctx, + )? != ValidationResult::Approved + { + return Err(MplCoreError::InvalidAuthority.into()); + } process_update_external_plugin_adapter( collection, - plugin, + external_plugin_adapter, args.key, args.update_info, plugin_header, @@ -172,10 +186,19 @@ fn process_update_external_plugin_adapter<'a, T: DataBlob + SolanaAccount>( let mut plugin_registry = plugin_registry.ok_or(MplCoreError::PluginsNotInitialized)?; let mut plugin_header = plugin_header.ok_or(MplCoreError::PluginsNotInitialized)?; - let plugin_registry_clone = plugin_registry.clone(); - let (_, record) = find_external_plugin_adapter(&plugin_registry_clone, &key, account)?; - let mut registry_record = record.ok_or(MplCoreError::PluginNotFound)?.clone(); + // Update the registry record using a mutable reference that ties back to `plugin_registry`. + let (_, record) = find_external_plugin_adapter_mut(&mut plugin_registry, &key, account)?; + let registry_record = record.ok_or(MplCoreError::PluginNotFound)?; + let old_registry_record_size = registry_record.try_to_vec()?.len() as isize; + registry_record.update(&update_info)?; + let new_registry_record_size = registry_record.try_to_vec()?.len() as isize; + let registry_record_size_diff = new_registry_record_size + .checked_sub(old_registry_record_size) + .ok_or(MplCoreError::NumericalOverflow)?; + + // Clone into a new copy, dropping the mutable reference so that `plugin_registry` can be mutably borrowed again later. + let registry_record = registry_record.clone(); let mut new_plugin = plugin.clone(); new_plugin.update(&update_info); @@ -185,19 +208,21 @@ fn process_update_external_plugin_adapter<'a, T: DataBlob + SolanaAccount>( // The difference in size between the new and old account which is used to calculate the new size of the account. let plugin_size = plugin_data.len() as isize; - let size_diff = (new_plugin_data.len() as isize) + let plugin_size_diff = (new_plugin_data.len() as isize) .checked_sub(plugin_size) .ok_or(MplCoreError::NumericalOverflow)?; // The new size of the account. let new_size = (account.data_len() as isize) - .checked_add(size_diff) + .checked_add(plugin_size_diff) + .ok_or(MplCoreError::NumericalOverflow)? + .checked_add(registry_record_size_diff) .ok_or(MplCoreError::NumericalOverflow)?; // The new offset of the plugin registry is the old offset plus the size difference. let registry_offset = plugin_header.plugin_registry_offset; let new_registry_offset = (registry_offset as isize) - .checked_add(size_diff) + .checked_add(plugin_size_diff) .ok_or(MplCoreError::NumericalOverflow)?; plugin_header.plugin_registry_offset = new_registry_offset as usize; @@ -207,7 +232,7 @@ fn process_update_external_plugin_adapter<'a, T: DataBlob + SolanaAccount>( .ok_or(MplCoreError::NumericalOverflow)?; let new_next_plugin_offset = next_plugin_offset - .checked_add(size_diff) + .checked_add(plugin_size_diff) .ok_or(MplCoreError::NumericalOverflow)?; // //TODO: This is memory intensive, we should use memmove instead probably. @@ -227,7 +252,7 @@ fn process_update_external_plugin_adapter<'a, T: DataBlob + SolanaAccount>( for record in &mut plugin_registry.external_registry { if registry_record.offset < record.offset { let new_offset = (record.offset as isize) - .checked_add(size_diff) + .checked_add(plugin_size_diff) .ok_or(MplCoreError::NumericalOverflow)?; record.offset = new_offset as usize; @@ -237,7 +262,7 @@ fn process_update_external_plugin_adapter<'a, T: DataBlob + SolanaAccount>( for record in &mut plugin_registry.registry { if registry_record.offset < record.offset { let new_offset = (record.offset as isize) - .checked_add(size_diff) + .checked_add(plugin_size_diff) .ok_or(MplCoreError::NumericalOverflow)?; record.offset = new_offset as usize; diff --git a/programs/mpl-core/src/state/asset.rs b/programs/mpl-core/src/state/asset.rs index 44655511..fac6b655 100644 --- a/programs/mpl-core/src/state/asset.rs +++ b/programs/mpl-core/src/state/asset.rs @@ -125,7 +125,7 @@ impl AssetV1 { /// Check permissions for the update external plugin adapter lifecycle event. pub fn check_update_external_plugin_adapter() -> CheckResult { - CheckResult::CanApprove + CheckResult::None } /// Validate the add plugin lifecycle event. @@ -328,15 +328,11 @@ impl AssetV1 { /// Validate the update external plugin adapter lifecycle event. pub fn validate_update_external_plugin_adapter( &self, - authority_info: &AccountInfo, + _authority_info: &AccountInfo, _: Option<&Plugin>, _plugin: Option<&ExternalPluginAdapter>, ) -> Result { - if self.update_authority == UpdateAuthority::Address(*authority_info.key) { - approve!() - } else { - abstain!() - } + abstain!() } } diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index c4056576..18803383 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -110,7 +110,7 @@ impl CollectionV1 { /// Check permissions for the update external plugin adapter lifecycle event. pub fn check_update_external_plugin_adapter() -> CheckResult { - CheckResult::CanApprove + CheckResult::None } /// Validate the add plugin lifecycle event. @@ -293,15 +293,11 @@ impl CollectionV1 { /// Validate the update external plugin adapter lifecycle event. pub fn validate_update_external_plugin_adapter( &self, - authority_info: &AccountInfo, + _authority_info: &AccountInfo, _: Option<&Plugin>, _plugin: Option<&ExternalPluginAdapter>, ) -> Result { - if self.update_authority == *authority_info.key { - approve!() - } else { - abstain!() - } + abstain!() } /// Increment size of the Collection