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/clients/js/test/revokeAuthority.test.ts b/clients/js/test/revokeAuthority.test.ts index 6be327ad..61ce649a 100644 --- a/clients/js/test/revokeAuthority.test.ts +++ b/clients/js/test/revokeAuthority.test.ts @@ -10,6 +10,8 @@ import { pluginAuthorityPair, approveCollectionPluginAuthorityV1, revokeCollectionPluginAuthorityV1, + updatePluginAuthority, + ownerPluginAuthority, } from '../src'; import { assertAsset, @@ -95,7 +97,7 @@ test('it can remove the default authority from a plugin to make it immutable', a }); }); -test('it can remove a pubkey authority from a plugin if that pubkey is the signer authority', async (t) => { +test('it can remove a pubkey authority from an owner-managed plugin if that pubkey is the signer authority', async (t) => { // Given a Umi instance and a new signer. const umi = await createUmi(); const pubkeyAuth = await generateSignerWithSol(umi); @@ -147,6 +149,157 @@ test('it can remove a pubkey authority from a plugin if that pubkey is the signe }); }); +test('it can remove an update authority from an owner-managed plugin if that pubkey is the signer authority', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const owner = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + plugins: [ + pluginAuthorityPair({ type: 'FreezeDelegate', data: { frozen: false } }), + ], + }); + + await approvePluginAuthorityV1(umi, { + asset: asset.publicKey, + authority: owner, + pluginType: PluginType.FreezeDelegate, + newAuthority: updatePluginAuthority(), + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + freezeDelegate: { + authority: { type: 'UpdateAuthority' }, + frozen: false, + }, + }); + + await revokePluginAuthorityV1(umi, { + asset: asset.publicKey, + pluginType: PluginType.FreezeDelegate, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + freezeDelegate: { + authority: { + type: 'Owner', + }, + frozen: false, + }, + }); +}); + +test('it can remove a pubkey authority from an authority-managed plugin if that pubkey is the signer authority', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const pubkeyAuth = await generateSignerWithSol(umi); + + const asset = await createAsset(umi, { + plugins: [ + pluginAuthorityPair({ type: 'Attributes', data: { attributeList: [] } }), + ], + }); + + await approvePluginAuthorityV1(umi, { + asset: asset.publicKey, + pluginType: PluginType.Attributes, + newAuthority: addressPluginAuthority(pubkeyAuth.publicKey), + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + attributes: { + authority: { + type: 'Address', + address: pubkeyAuth.publicKey, + }, + attributeList: [], + }, + }); + + const umi2 = await createUmi(); + + await revokePluginAuthorityV1(umi2, { + payer: umi2.identity, + asset: asset.publicKey, + authority: pubkeyAuth, + pluginType: PluginType.Attributes, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [], + }, + }); +}); + +test('it can remove an owner authority from an authority-managed plugin if that pubkey is the signer authority', async (t) => { + // Given a Umi instance and a new signer. + const umi = await createUmi(); + const owner = generateSigner(umi); + + const asset = await createAsset(umi, { + owner: owner.publicKey, + plugins: [ + pluginAuthorityPair({ type: 'Attributes', data: { attributeList: [] } }), + ], + }); + + await approvePluginAuthorityV1(umi, { + asset: asset.publicKey, + pluginType: PluginType.Attributes, + newAuthority: ownerPluginAuthority(), + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + attributes: { + authority: { + type: 'Owner', + }, + attributeList: [], + }, + }); + + const umi2 = await createUmi(); + + await revokePluginAuthorityV1(umi2, { + payer: umi2.identity, + asset: asset.publicKey, + authority: owner, + pluginType: PluginType.Attributes, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + asset: asset.publicKey, + owner: owner.publicKey, + updateAuthority: { type: 'Address', address: umi.identity.publicKey }, + attributes: { + authority: { + type: 'UpdateAuthority', + }, + attributeList: [], + }, + }); +}); + test('it cannot remove a none authority from a plugin', async (t) => { // Given a Umi instance and a new signer. const umi = await createUmi(); diff --git a/clients/rust/src/hooked/plugin.rs b/clients/rust/src/hooked/plugin.rs index 1a8cd933..9f815451 100644 --- a/clients/rust/src/hooked/plugin.rs +++ b/clients/rust/src/hooked/plugin.rs @@ -6,7 +6,7 @@ use num_traits::FromPrimitive; use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; use crate::{ - accounts::{BaseAssetV1, PluginHeaderV1}, + accounts::{BaseAssetV1, BaseCollectionV1, PluginHeaderV1}, errors::MplCoreError, types::{ ExternalPluginAdapter, ExternalPluginAdapterKey, ExternalPluginAdapterType, LinkedDataKey, @@ -84,6 +84,22 @@ pub fn fetch_plugin( )) } +/// Fetch the plugin on an asset. +pub fn fetch_asset_plugin( + account: &AccountInfo, + plugin_type: PluginType, +) -> Result<(PluginAuthority, U, usize), std::io::Error> { + fetch_plugin::(account, plugin_type) +} + +/// Fetch the plugin on a collection. +pub fn fetch_collection_plugin( + account: &AccountInfo, + plugin_type: PluginType, +) -> Result<(PluginAuthority, U, usize), std::io::Error> { + fetch_plugin::(account, plugin_type) +} + /// Fetch the plugin registry, dropping any unknown plugins (i.e. `PluginType`s that are too new /// for this client to know about). pub fn fetch_plugins(account_data: &[u8]) -> Result, std::io::Error> { diff --git a/programs/mpl-core/src/plugins/autograph.rs b/programs/mpl-core/src/plugins/autograph.rs index 673098d1..37825e6c 100644 --- a/programs/mpl-core/src/plugins/autograph.rs +++ b/programs/mpl-core/src/plugins/autograph.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, HashSet}; use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{program_error::ProgramError, pubkey::Pubkey}; -use crate::{error::MplCoreError, plugins::PluginType, state::Authority}; +use crate::error::MplCoreError; use super::{ abstain, approve, Plugin, PluginValidation, PluginValidationContext, ValidationResult, @@ -120,22 +120,4 @@ impl PluginValidation for Autograph { _ => abstain!(), } } - - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::Autograph - { - approve!() - } else { - abstain!() - } - } } diff --git a/programs/mpl-core/src/plugins/burn_delegate.rs b/programs/mpl-core/src/plugins/burn_delegate.rs index 5d62f615..0d6c145a 100644 --- a/programs/mpl-core/src/plugins/burn_delegate.rs +++ b/programs/mpl-core/src/plugins/burn_delegate.rs @@ -2,7 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::program_error::ProgramError; use crate::{ - plugins::{abstain, approve, PluginType}, + plugins::{abstain, approve}, state::{Authority, DataBlob}, }; @@ -52,22 +52,4 @@ impl PluginValidation for BurnDelegate { abstain!() } } - - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::BurnDelegate - { - approve!() - } else { - abstain!() - } - } } diff --git a/programs/mpl-core/src/plugins/edition.rs b/programs/mpl-core/src/plugins/edition.rs index 11e6afb2..077d48d5 100644 --- a/programs/mpl-core/src/plugins/edition.rs +++ b/programs/mpl-core/src/plugins/edition.rs @@ -1,12 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::program_error::ProgramError; -use crate::{plugins::approve, state::Authority}; - -use super::{ - abstain, reject, Plugin, PluginType, PluginValidation, PluginValidationContext, - ValidationResult, -}; +use super::{abstain, reject, Plugin, PluginValidation, PluginValidationContext, ValidationResult}; /// The edition plugin allows the creator to set an edition number on the asset /// The default authority for this plugin is the creator. @@ -45,21 +40,4 @@ impl PluginValidation for Edition { _ => abstain!(), } } - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::Edition - { - approve!() - } else { - abstain!() - } - } } diff --git a/programs/mpl-core/src/plugins/external_plugin_adapters.rs b/programs/mpl-core/src/plugins/external_plugin_adapters.rs index 5a2209dd..a20a9154 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}, }; @@ -271,6 +272,61 @@ 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::AppData(app_data) => { + app_data.validate_update_external_plugin_adapter(ctx) + } + ExternalPluginAdapter::LinkedLifecycleHook(lifecycle_hook) => { + lifecycle_hook.validate_update_external_plugin_adapter(ctx) + } + ExternalPluginAdapter::LinkedAppData(app_data) => { + app_data.validate_update_external_plugin_adapter(ctx) + } + // Here we block the update of a DataSection plugin because this is only done internally. + ExternalPluginAdapter::DataSection(_) => Ok(ValidationResult::Rejected), + }?; + + 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 8c488efd..2f47c5ed 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -49,7 +49,7 @@ impl ExternalCheckResult { /// Bitfield representation of lifecycle permissions for external plugin adapter, third party plugins. #[bitfield(bits = 32)] -#[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[derive(Eq, PartialEq, Copy, Clone, Debug, Default)] pub struct ExternalCheckResultBits { pub can_listen: bool, pub can_approve: bool, @@ -342,15 +342,29 @@ impl Plugin { plugin: &Plugin, ctx: &PluginValidationContext, ) -> Result { + let target_plugin = ctx.target_plugin.ok_or(MplCoreError::InvalidPlugin)?; + // If the plugin being checked is Authority::None then it can't be revoked. if ctx.self_authority == &Authority::None - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::from(plugin) + && PluginType::from(target_plugin) == PluginType::from(plugin) { return reject!(); } - match plugin { + let base_result = if PluginType::from(target_plugin) == PluginType::from(plugin) + && ctx.resolved_authorities.is_some() + && ctx + .resolved_authorities + .unwrap() + .contains(ctx.self_authority) + { + solana_program::msg!("Base: Approved"); + ValidationResult::Approved + } else { + ValidationResult::Pass + }; + + let result = match plugin { Plugin::Royalties(royalties) => royalties.validate_revoke_plugin_authority(ctx), Plugin::FreezeDelegate(freeze) => freeze.validate_revoke_plugin_authority(ctx), Plugin::BurnDelegate(burn) => burn.validate_revoke_plugin_authority(ctx), @@ -380,6 +394,12 @@ impl Plugin { verified_creators.validate_revoke_plugin_authority(ctx) } Plugin::Autograph(autograph) => autograph.validate_revoke_plugin_authority(ctx), + }?; + + if result == ValidationResult::Pass { + Ok(base_result) + } else { + Ok(result) } } @@ -714,80 +734,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 @@ -1001,22 +947,6 @@ pub(crate) trait PluginValidation { abstain!() } - /// Validate the add_authority lifecycle action. - fn validate_add_authority( - &self, - _ctx: &PluginValidationContext, - ) -> Result { - abstain!() - } - - /// Validate the add_authority lifecycle action. - fn validate_remove_authority( - &self, - _ctx: &PluginValidationContext, - ) -> Result { - abstain!() - } - /// Validate the update_plugin lifecycle action. fn validate_update_external_plugin_adapter( &self, diff --git a/programs/mpl-core/src/plugins/permanent_burn_delegate.rs b/programs/mpl-core/src/plugins/permanent_burn_delegate.rs index 348b9f07..8a418614 100644 --- a/programs/mpl-core/src/plugins/permanent_burn_delegate.rs +++ b/programs/mpl-core/src/plugins/permanent_burn_delegate.rs @@ -4,7 +4,7 @@ use solana_program::program_error::ProgramError; use crate::state::DataBlob; use super::{ - abstain, approve, force_approve, reject, PluginType, PluginValidation, PluginValidationContext, + abstain, force_approve, reject, PluginType, PluginValidation, PluginValidationContext, ValidationResult, }; @@ -40,13 +40,6 @@ impl PluginValidation for PermanentBurnDelegate { } } - fn validate_revoke_plugin_authority( - &self, - _ctx: &PluginValidationContext, - ) -> Result { - approve!() - } - fn validate_burn( &self, ctx: &PluginValidationContext, diff --git a/programs/mpl-core/src/plugins/permanent_freeze_delegate.rs b/programs/mpl-core/src/plugins/permanent_freeze_delegate.rs index 432482c8..80bbd556 100644 --- a/programs/mpl-core/src/plugins/permanent_freeze_delegate.rs +++ b/programs/mpl-core/src/plugins/permanent_freeze_delegate.rs @@ -3,10 +3,10 @@ use solana_program::program_error::ProgramError; use crate::{ plugins::{reject, PluginType}, - state::{Authority, DataBlob}, + state::DataBlob, }; -use super::{abstain, approve, PluginValidation, PluginValidationContext, ValidationResult}; +use super::{abstain, PluginValidation, PluginValidationContext, ValidationResult}; /// The permanent freeze plugin allows any authority to lock the asset so it's no longer transferable. /// The default authority for this plugin is the update authority. @@ -78,24 +78,6 @@ impl PluginValidation for PermanentFreezeDelegate { } } - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::PermanentFreezeDelegate - { - approve!() - } else { - abstain!() - } - } - /// Validate the remove plugin lifecycle action. fn validate_remove_plugin( &self, diff --git a/programs/mpl-core/src/plugins/permanent_transfer_delegate.rs b/programs/mpl-core/src/plugins/permanent_transfer_delegate.rs index f7382033..8f7a4a85 100644 --- a/programs/mpl-core/src/plugins/permanent_transfer_delegate.rs +++ b/programs/mpl-core/src/plugins/permanent_transfer_delegate.rs @@ -4,7 +4,7 @@ use solana_program::program_error::ProgramError; use crate::state::DataBlob; use super::{ - abstain, approve, force_approve, reject, PluginType, PluginValidation, PluginValidationContext, + abstain, force_approve, reject, PluginType, PluginValidation, PluginValidationContext, ValidationResult, }; @@ -40,13 +40,6 @@ impl PluginValidation for PermanentTransferDelegate { } } - fn validate_revoke_plugin_authority( - &self, - _ctx: &PluginValidationContext, - ) -> Result { - approve!() - } - fn validate_transfer( &self, ctx: &PluginValidationContext, diff --git a/programs/mpl-core/src/plugins/royalties.rs b/programs/mpl-core/src/plugins/royalties.rs index ec93acb5..c8e3a4d2 100644 --- a/programs/mpl-core/src/plugins/royalties.rs +++ b/programs/mpl-core/src/plugins/royalties.rs @@ -3,11 +3,9 @@ use std::collections::HashSet; use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{program_error::ProgramError, pubkey::Pubkey}; -use crate::{error::MplCoreError, plugins::PluginType, state::Authority}; +use crate::error::MplCoreError; -use super::{ - abstain, approve, reject, Plugin, PluginValidation, PluginValidationContext, ValidationResult, -}; +use super::{abstain, reject, Plugin, PluginValidation, PluginValidationContext, ValidationResult}; /// The creator on an asset and whether or not they are verified. #[derive(Clone, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] @@ -132,22 +130,4 @@ impl PluginValidation for Royalties { abstain!() } } - - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::Royalties - { - approve!() - } else { - abstain!() - } - } } diff --git a/programs/mpl-core/src/plugins/transfer.rs b/programs/mpl-core/src/plugins/transfer.rs index b722d060..033dc047 100644 --- a/programs/mpl-core/src/plugins/transfer.rs +++ b/programs/mpl-core/src/plugins/transfer.rs @@ -1,10 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::program_error::ProgramError; -use crate::{ - plugins::PluginType, - state::{Authority, DataBlob}, -}; +use crate::state::{Authority, DataBlob}; use super::{abstain, approve, PluginValidation, PluginValidationContext, ValidationResult}; @@ -75,22 +72,4 @@ impl PluginValidation for TransferDelegate { } abstain!() } - - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::TransferDelegate - { - approve!() - } else { - abstain!() - } - } } diff --git a/programs/mpl-core/src/plugins/utils.rs b/programs/mpl-core/src/plugins/utils.rs index 71f18ec1..5dd60a9a 100644 --- a/programs/mpl-core/src/plugins/utils.rs +++ b/programs/mpl-core/src/plugins/utils.rs @@ -810,69 +810,82 @@ 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) - | ExternalPluginAdapterKey::LinkedLifecycleHook(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::AppData(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()), - } - } - ExternalPluginAdapterKey::LinkedAppData(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()), - } - } - ExternalPluginAdapterKey::DataSection(linked_data_key) => { - let linked_data_key_offset = record - .offset - .checked_add(1) - .ok_or(MplCoreError::NumericalOverflow)?; - linked_data_key - == &match LinkedDataKey::deserialize( - &mut &account.data.borrow()[linked_data_key_offset..], - ) { - Ok(linked_data_key) => linked_data_key, - 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) + | ExternalPluginAdapterKey::LinkedLifecycleHook(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::AppData(authority) + | ExternalPluginAdapterKey::LinkedAppData(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()), + } + } + ExternalPluginAdapterKey::DataSection(linked_data_key) => { + let linked_data_key_offset = record_ref + .offset + .checked_add(1) + .ok_or(MplCoreError::NumericalOverflow)?; + linked_data_key + == &match LinkedDataKey::deserialize( + &mut &account.data.borrow()[linked_data_key_offset..], + ) { + Ok(linked_data_key) => linked_data_key, + Err(_) => return Err(MplCoreError::DeserializationError.into()), + } + } + }) + { + Ok(true) + } else { + Ok(false) + } } diff --git a/programs/mpl-core/src/plugins/verified_creators.rs b/programs/mpl-core/src/plugins/verified_creators.rs index 66719fa8..b88da0e7 100644 --- a/programs/mpl-core/src/plugins/verified_creators.rs +++ b/programs/mpl-core/src/plugins/verified_creators.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, HashSet}; use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::{program_error::ProgramError, pubkey::Pubkey}; -use crate::{error::MplCoreError, plugins::PluginType, state::Authority}; +use crate::error::MplCoreError; use super::{abstain, Plugin, PluginValidation, PluginValidationContext, ValidationResult}; @@ -196,23 +196,4 @@ impl PluginValidation for VerifiedCreators { _ => abstain!(), } } - - /// Validate the revoke plugin authority lifecycle action. - fn validate_revoke_plugin_authority( - &self, - ctx: &PluginValidationContext, - ) -> Result { - if ctx.self_authority - == &(Authority::Address { - address: *ctx.authority_info.key, - }) - && ctx.target_plugin.is_some() - && PluginType::from(ctx.target_plugin.unwrap()) == PluginType::VerifiedCreators - { - solana_program::msg!("Verified creators: Approved"); - Ok(ValidationResult::Approved) - } else { - abstain!() - } - } } 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 dbffde1b..ed6d8622 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,34 +56,40 @@ 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, 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, - 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, + new_asset_authority: None, + new_collection_authority: 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, @@ -124,31 +130,41 @@ 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, external_plugin_adapter) = + fetch_wrapped_external_plugin_adapter::( + ctx.accounts.collection, + None, + &args.key, + )?; + + let validation_ctx = PluginValidationContext { accounts, - authority, - ctx.accounts.collection, - None, - 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, + new_asset_authority: None, + new_collection_authority: 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, @@ -174,10 +190,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); @@ -187,19 +212,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; @@ -209,7 +236,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. @@ -226,7 +253,7 @@ fn process_update_external_plugin_adapter<'a, T: DataBlob + SolanaAccount>( plugin_header.save(account, core.get_size())?; // Move offsets for existing registry records. - plugin_registry.bump_offsets(registry_record.offset, size_diff)?; + plugin_registry.bump_offsets(registry_record.offset, plugin_size_diff)?; plugin_registry.save(account, new_registry_offset as usize)?; new_plugin.save(account, registry_record.offset)?; 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 dce8f2ab..acc2115c 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 number of minted items of the Collection