From 51f076b588026acba4d55f1364bc2eede566518d Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Thu, 25 Jul 2024 17:05:00 -0400 Subject: [PATCH] Fixing create IXes to properly run create validations. --- clients/js/test/createCollection.test.ts | 2 +- .../js/test/externalPlugins/oracle.test.ts | 161 ++++++++++++++++++ .../mpl-core/src/plugins/update_delegate.rs | 9 +- programs/mpl-core/src/processor/create.rs | 30 +++- .../src/processor/create_collection.rs | 29 +--- programs/mpl-core/src/state/asset.rs | 19 +++ programs/mpl-core/src/state/collection.rs | 19 +++ .../mpl-core/src/state/update_authority.rs | 67 +------- 8 files changed, 236 insertions(+), 100 deletions(-) diff --git a/clients/js/test/createCollection.test.ts b/clients/js/test/createCollection.test.ts index 15578446..2c8fff14 100644 --- a/clients/js/test/createCollection.test.ts +++ b/clients/js/test/createCollection.test.ts @@ -145,7 +145,7 @@ test('it cannot create a new asset with a collection if it is not the collection collection: collection.publicKey, }); - await t.throwsAsync(result, { name: 'InvalidAuthority' }); + await t.throwsAsync(result, { name: 'NoApprovals' }); }); test('it cannot create a collection with an owner managed plugin', async (t) => { diff --git a/clients/js/test/externalPlugins/oracle.test.ts b/clients/js/test/externalPlugins/oracle.test.ts index 49cbaa0d..51d7bb32 100644 --- a/clients/js/test/externalPlugins/oracle.test.ts +++ b/clients/js/test/externalPlugins/oracle.test.ts @@ -52,6 +52,7 @@ import { fetchAssetV1, ExternalValidationResult, ruleSet, + fetchCollection, } from '../../src'; const createUmi = async () => @@ -3615,3 +3616,163 @@ test('it cannot update oracle on collection using update authority when differen ], }); }); + +test('it can create an oracle on a collection with create set to reject', async (t) => { + const umi = await createUmi(); + const oracleSigner = generateSigner(umi); + await fixedAccountInit(umi, { + signer: umi.identity, + account: oracleSigner, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Rejected, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const collection = generateSigner(umi); + await createCollection(umi, { + collection, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + }, + ], + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + uri: 'https://example.com', + name: 'Test name', + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + oracles: [ + { + authority: { + type: 'UpdateAuthority', + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + }, + baseAddress: oracleSigner.publicKey, + baseAddressConfig: undefined, + }, + ], + }); +}); + +test('it can use fixed address oracle on a collection to deny create', async (t) => { + const umi = await createUmi(); + const account = generateSigner(umi); + + // write to example program oracle account + await fixedAccountInit(umi, { + account, + signer: umi.identity, + payer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Rejected, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + const collectionSigner = generateSigner(umi); + await createCollection(umi, { + collection: collectionSigner, + name: 'Test name', + uri: 'https://example.com', + plugins: [ + { + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + }, + baseAddress: account.publicKey, + }, + ], + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + uri: 'https://example.com', + name: 'Test name', + collection: collectionSigner.publicKey, + updateAuthority: umi.identity.publicKey, + oracles: [ + { + authority: { + type: 'UpdateAuthority', + }, + type: 'Oracle', + resultsOffset: { + type: 'Anchor', + }, + lifecycleChecks: { + create: [CheckResult.CAN_REJECT], + }, + baseAddress: account.publicKey, + baseAddressConfig: undefined, + }, + ], + }); + + const collection = await fetchCollection(umi, collectionSigner.publicKey); + + // create asset referencing the oracle account + const assetSigner = generateSigner(umi); + const result = create(umi, { + ...DEFAULT_ASSET, + asset: assetSigner, + collection, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + await fixedAccountSet(umi, { + account: account.publicKey, + signer: umi.identity, + args: { + oracleData: { + __kind: 'V1', + create: ExternalValidationResult.Pass, + update: ExternalValidationResult.Pass, + transfer: ExternalValidationResult.Pass, + burn: ExternalValidationResult.Pass, + }, + }, + }).sendAndConfirm(umi); + + await create(umi, { + ...DEFAULT_ASSET, + asset: assetSigner, + collection, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: assetSigner.publicKey, + owner: umi.identity.publicKey, + }); +}); diff --git a/programs/mpl-core/src/plugins/update_delegate.rs b/programs/mpl-core/src/plugins/update_delegate.rs index 2ee4fd5a..dbf268bf 100644 --- a/programs/mpl-core/src/plugins/update_delegate.rs +++ b/programs/mpl-core/src/plugins/update_delegate.rs @@ -47,11 +47,18 @@ impl DataBlob for UpdateDelegate { impl PluginValidation for UpdateDelegate { fn validate_create( &self, - _ctx: &PluginValidationContext, + ctx: &PluginValidationContext, ) -> Result { if !self.additional_delegates.is_empty() { return Err(MplCoreError::NotAvailable.into()); } + + if let Some(resolved_authorities) = ctx.resolved_authorities { + if resolved_authorities.contains(ctx.self_authority) { + return approve!(); + } + } + abstain!() } diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 5ecea2f2..cb2021f5 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -11,13 +11,13 @@ use crate::{ plugins::{ create_meta_idempotent, create_plugin_meta, initialize_external_plugin_adapter, initialize_plugin, CheckResult, ExternalCheckResultBits, ExternalPluginAdapter, - ExternalPluginAdapterInitInfo, Plugin, PluginAuthorityPair, PluginType, - PluginValidationContext, ValidationResult, + ExternalPluginAdapterInitInfo, HookableLifecycleEvent, Plugin, PluginAuthorityPair, + PluginType, PluginValidationContext, ValidationResult, }, state::{ AssetV1, Authority, CollectionV1, DataState, SolanaAccount, UpdateAuthority, COLLECT_AMOUNT, }, - utils::resolve_authority, + utils::{resolve_authority, validate_asset_permissions}, }; #[repr(C)] @@ -101,10 +101,6 @@ pub(crate) fn process_create<'a>( ), }; - if update_authority.validate_create(&ctx.accounts, &args)? == ValidationResult::Rejected { - return Err(MplCoreError::InvalidAuthority.into()); - } - let new_asset = AssetV1::new( *ctx.accounts .owner @@ -151,6 +147,26 @@ pub(crate) fn process_create<'a>( ); if args.data_state == DataState::AccountState { + // Validate asset permissions. + let _ = validate_asset_permissions( + accounts, + authority, + ctx.accounts.asset, + ctx.accounts.collection, + None, + None, + None, + AssetV1::check_create, + CollectionV1::check_create, + PluginType::check_create, + AssetV1::validate_create, + CollectionV1::validate_create, + Plugin::validate_create, + Some(ExternalPluginAdapter::validate_create), + Some(HookableLifecycleEvent::Create), + )?; + + // Validate permissions for the created asset. let mut approved = true; let mut force_approved = false; diff --git a/programs/mpl-core/src/processor/create_collection.rs b/programs/mpl-core/src/processor/create_collection.rs index dd907718..ec57af7d 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -10,9 +10,8 @@ use crate::{ instruction::accounts::CreateCollectionV2Accounts, plugins::{ create_meta_idempotent, create_plugin_meta, initialize_external_plugin_adapter, - initialize_plugin, CheckResult, ExternalCheckResultBits, ExternalPluginAdapter, - ExternalPluginAdapterInitInfo, Plugin, PluginAuthorityPair, PluginType, - PluginValidationContext, ValidationResult, + initialize_plugin, CheckResult, ExternalPluginAdapterInitInfo, Plugin, PluginAuthorityPair, + PluginType, PluginValidationContext, ValidationResult, }, state::{Authority, CollectionV1, Key}, }; @@ -170,30 +169,6 @@ pub(crate) fn process_create_collection<'a>( ctx.accounts.system_program, )?; for plugin_init_info in &plugins { - let external_check_result_bits = ExternalCheckResultBits::from( - ExternalPluginAdapter::check_create(plugin_init_info), - ); - - if external_check_result_bits.can_reject() { - let validation_ctx = PluginValidationContext { - accounts, - asset_info: None, - collection_info: Some(ctx.accounts.collection), - // External plugin adapters are always managed by the update authority. - self_authority: &Authority::UpdateAuthority, - authority_info: authority, - resolved_authorities: None, - new_owner: None, - target_plugin: None, - }; - if ExternalPluginAdapter::validate_create( - &ExternalPluginAdapter::from(plugin_init_info), - &validation_ctx, - )? == ValidationResult::Rejected - { - approved = false; - }; - } initialize_external_plugin_adapter::( plugin_init_info, &mut plugin_header, diff --git a/programs/mpl-core/src/state/asset.rs b/programs/mpl-core/src/state/asset.rs index fac6b655..30ec3c56 100644 --- a/programs/mpl-core/src/state/asset.rs +++ b/programs/mpl-core/src/state/asset.rs @@ -63,6 +63,11 @@ impl AssetV1 { /// The base length of the asset account with an empty name and uri and no seq. pub const BASE_LENGTH: usize = 1 + 32 + 33 + 4 + 4 + 1; + /// Check permissions for the create lifecycle event. + pub fn check_create() -> CheckResult { + CheckResult::CanApprove + } + /// Check permissions for the add plugin lifecycle event. pub fn check_add_plugin() -> CheckResult { CheckResult::CanApprove @@ -128,6 +133,20 @@ impl AssetV1 { CheckResult::None } + /// Validate the create lifecycle event. + pub fn validate_create( + &self, + _authority_info: &AccountInfo, + _new_plugin: Option<&Plugin>, + _: Option<&ExternalPluginAdapter>, + ) -> Result { + // If the asset is part of a collection, the collection must approve the create. + match self.update_authority { + UpdateAuthority::Collection(_) => abstain!(), + _ => approve!(), + } + } + /// Validate the add plugin lifecycle event. pub fn validate_add_plugin( &self, diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index 18803383..390d2729 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -48,6 +48,11 @@ impl CollectionV1 { } } + /// Check permissions for the create lifecycle event. + pub fn check_create() -> CheckResult { + CheckResult::CanApprove + } + /// Check permissions for the add plugin lifecycle event. pub fn check_add_plugin() -> CheckResult { CheckResult::CanApprove @@ -113,6 +118,20 @@ impl CollectionV1 { CheckResult::None } + /// Validate the create lifecycle event. + pub fn validate_create( + &self, + authority_info: &AccountInfo, + _new_plugin: Option<&Plugin>, + _: Option<&ExternalPluginAdapter>, + ) -> Result { + if authority_info.key == &self.update_authority { + approve!() + } else { + abstain!() + } + } + /// Validate the add plugin lifecycle event. pub fn validate_add_plugin( &self, diff --git a/programs/mpl-core/src/state/update_authority.rs b/programs/mpl-core/src/state/update_authority.rs index b1acd03f..86b0727b 100644 --- a/programs/mpl-core/src/state/update_authority.rs +++ b/programs/mpl-core/src/state/update_authority.rs @@ -1,20 +1,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use mpl_utils::assert_signer; use solana_program::{program_error::ProgramError, pubkey::Pubkey}; use crate::{ - error::MplCoreError, instruction::accounts::{ - BurnV1Accounts, CompressV1Accounts, CreateV2Accounts, DecompressV1Accounts, - TransferV1Accounts, UpdateV1Accounts, + BurnV1Accounts, CompressV1Accounts, DecompressV1Accounts, TransferV1Accounts, + UpdateV1Accounts, }, - plugins::{ - abstain, approve, fetch_plugin, reject, CheckResult, PluginType, UpdateDelegate, - ValidationResult, - }, - processor::CreateV2Args, - state::{Authority, CollectionV1, SolanaAccount}, - utils::assert_collection_authority, + plugins::{abstain, approve, reject, CheckResult, ValidationResult}, }; /// An enum representing the types of accounts that can update data on an asset. @@ -48,59 +40,6 @@ impl UpdateAuthority { CheckResult::CanApprove } - /// Validate the create lifecycle event. - pub(crate) fn validate_create( - &self, - ctx: &CreateV2Accounts, - _args: &CreateV2Args, - ) -> Result { - match (ctx.collection, self) { - // If you're trying to add a collection, then check the authority. - (Some(collection_info), UpdateAuthority::Collection(collection_address)) => { - if collection_info.key != collection_address { - return Err(MplCoreError::InvalidCollection.into()); - } - let collection = CollectionV1::load(collection_info, 0)?; - - let authority_info = match ctx.authority { - Some(authority) => { - assert_signer(authority)?; - authority - } - None => ctx.payer, - }; - - let maybe_update_delegate = fetch_plugin::( - collection_info, - PluginType::UpdateDelegate, - ); - - if let Ok((authority, _, _)) = maybe_update_delegate { - if assert_collection_authority(&collection, authority_info, &authority).is_err() - && assert_collection_authority( - &collection, - authority_info, - &Authority::UpdateAuthority, - ) - .is_err() - { - solana_program::msg!("UA: Rejected"); - return reject!(); - } - } else if authority_info.key != &collection.update_authority { - solana_program::msg!("UA: Rejected"); - return reject!(); - } - - abstain!() - } - // If you're not trying add a collection, then just pass. - (_, UpdateAuthority::Address(_)) => abstain!(), - // Otherwise reject because you're doing something weird. - _ => reject!(), - } - } - /// Validate the update lifecycle event. pub fn validate_update( &self,