diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 0c7414a4..4254682a 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -689,6 +689,69 @@ export class CannotAddDataSectionError extends ProgramError { codeToErrorMap.set(0x2f, CannotAddDataSectionError); nameToErrorMap.set('CannotAddDataSection', CannotAddDataSectionError); +/** PluginNotAllowedOnAsset: Plugin is not allowed to be added to an Asset */ +export class PluginNotAllowedOnAssetError extends ProgramError { + override readonly name: string = 'PluginNotAllowedOnAsset'; + + readonly code: number = 0x30; // 48 + + constructor(program: Program, cause?: Error) { + super('Plugin is not allowed to be added to an Asset', program, cause); + } +} +codeToErrorMap.set(0x30, PluginNotAllowedOnAssetError); +nameToErrorMap.set('PluginNotAllowedOnAsset', PluginNotAllowedOnAssetError); + +/** PluginNotAllowedOnCollection: Plugin is not allowed to be added to a Collection */ +export class PluginNotAllowedOnCollectionError extends ProgramError { + override readonly name: string = 'PluginNotAllowedOnCollection'; + + readonly code: number = 0x31; // 49 + + constructor(program: Program, cause?: Error) { + super('Plugin is not allowed to be added to a Collection', program, cause); + } +} +codeToErrorMap.set(0x31, PluginNotAllowedOnCollectionError); +nameToErrorMap.set( + 'PluginNotAllowedOnCollection', + PluginNotAllowedOnCollectionError +); + +/** InvalidTreasuryWithdrawn: Cannot add a treasury plugin with nonzero withdrawn amount */ +export class InvalidTreasuryWithdrawnError extends ProgramError { + override readonly name: string = 'InvalidTreasuryWithdrawn'; + + readonly code: number = 0x32; // 50 + + constructor(program: Program, cause?: Error) { + super( + 'Cannot add a treasury plugin with nonzero withdrawn amount', + program, + cause + ); + } +} +codeToErrorMap.set(0x32, InvalidTreasuryWithdrawnError); +nameToErrorMap.set('InvalidTreasuryWithdrawn', InvalidTreasuryWithdrawnError); + +/** CannotOverdraw: Cannot withdraw more than excess rent from treasury */ +export class CannotOverdrawError extends ProgramError { + override readonly name: string = 'CannotOverdraw'; + + readonly code: number = 0x33; // 51 + + constructor(program: Program, cause?: Error) { + super( + 'Cannot withdraw more than excess rent from treasury', + program, + cause + ); + } +} +codeToErrorMap.set(0x33, CannotOverdrawError); +nameToErrorMap.set('CannotOverdraw', CannotOverdrawError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/src/generated/types/baseTreasury.ts b/clients/js/src/generated/types/baseTreasury.ts new file mode 100644 index 00000000..a33dd03f --- /dev/null +++ b/clients/js/src/generated/types/baseTreasury.ts @@ -0,0 +1,22 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { Serializer, struct, u64 } from '@metaplex-foundation/umi/serializers'; + +export type BaseTreasury = { withdrawn: bigint }; + +export type BaseTreasuryArgs = { withdrawn: number | bigint }; + +export function getBaseTreasurySerializer(): Serializer< + BaseTreasuryArgs, + BaseTreasury +> { + return struct([['withdrawn', u64()]], { + description: 'BaseTreasury', + }) as Serializer; +} diff --git a/clients/js/src/generated/types/index.ts b/clients/js/src/generated/types/index.ts index 86c9b374..398c4b1a 100644 --- a/clients/js/src/generated/types/index.ts +++ b/clients/js/src/generated/types/index.ts @@ -39,6 +39,7 @@ export * from './basePluginAuthority'; export * from './baseRoyalties'; export * from './baseRuleSet'; export * from './baseSeed'; +export * from './baseTreasury'; export * from './baseUpdateAuthority'; export * from './baseValidationResultsOffset'; export * from './burnDelegate'; diff --git a/clients/js/src/generated/types/plugin.ts b/clients/js/src/generated/types/plugin.ts index b5e2a9bb..ad554bf1 100644 --- a/clients/js/src/generated/types/plugin.ts +++ b/clients/js/src/generated/types/plugin.ts @@ -25,6 +25,8 @@ import { BaseMasterEditionArgs, BaseRoyalties, BaseRoyaltiesArgs, + BaseTreasury, + BaseTreasuryArgs, BurnDelegate, BurnDelegateArgs, Edition, @@ -50,6 +52,7 @@ import { getAutographSerializer, getBaseMasterEditionSerializer, getBaseRoyaltiesSerializer, + getBaseTreasurySerializer, getBurnDelegateSerializer, getEditionSerializer, getFreezeDelegateSerializer, @@ -77,7 +80,8 @@ export type Plugin = | { __kind: 'AddBlocker'; fields: [AddBlocker] } | { __kind: 'ImmutableMetadata'; fields: [ImmutableMetadata] } | { __kind: 'VerifiedCreators'; fields: [VerifiedCreators] } - | { __kind: 'Autograph'; fields: [Autograph] }; + | { __kind: 'Autograph'; fields: [Autograph] } + | { __kind: 'Treasury'; fields: [BaseTreasury] }; export type PluginArgs = | { __kind: 'Royalties'; fields: [BaseRoyaltiesArgs] } @@ -97,7 +101,8 @@ export type PluginArgs = | { __kind: 'AddBlocker'; fields: [AddBlockerArgs] } | { __kind: 'ImmutableMetadata'; fields: [ImmutableMetadataArgs] } | { __kind: 'VerifiedCreators'; fields: [VerifiedCreatorsArgs] } - | { __kind: 'Autograph'; fields: [AutographArgs] }; + | { __kind: 'Autograph'; fields: [AutographArgs] } + | { __kind: 'Treasury'; fields: [BaseTreasuryArgs] }; export function getPluginSerializer(): Serializer { return dataEnum( @@ -192,6 +197,12 @@ export function getPluginSerializer(): Serializer { ['fields', tuple([getAutographSerializer()])], ]), ], + [ + 'Treasury', + struct>([ + ['fields', tuple([getBaseTreasurySerializer()])], + ]), + ], ], { description: 'Plugin' } ) as Serializer; @@ -261,6 +272,10 @@ export function plugin( kind: 'Autograph', data: GetDataEnumKindContent['fields'] ): GetDataEnumKind; +export function plugin( + kind: 'Treasury', + data: GetDataEnumKindContent['fields'] +): GetDataEnumKind; export function plugin( kind: K, data?: any diff --git a/clients/js/src/generated/types/pluginType.ts b/clients/js/src/generated/types/pluginType.ts index a001175d..e43190ff 100644 --- a/clients/js/src/generated/types/pluginType.ts +++ b/clients/js/src/generated/types/pluginType.ts @@ -24,6 +24,7 @@ export enum PluginType { ImmutableMetadata, VerifiedCreators, Autograph, + Treasury, } export type PluginTypeArgs = PluginType; diff --git a/clients/js/src/plugins/lib.ts b/clients/js/src/plugins/lib.ts index b91e9541..961f3e29 100644 --- a/clients/js/src/plugins/lib.ts +++ b/clients/js/src/plugins/lib.ts @@ -27,6 +27,7 @@ import { } from './pluginAuthority'; import { royaltiesFromBase, royaltiesToBase } from './royalties'; import { masterEditionFromBase, masterEditionToBase } from './masterEdition'; +import { treasuryFromBase, treasuryToBase } from './treasury'; export function formPluginHeaderV1( pluginRegistryOffset: bigint @@ -103,6 +104,12 @@ export function createPluginV2(args: AssetAllPluginArgsV2): BasePlugin { fields: [masterEditionToBase(args)], }; } + if (type === 'Treasury') { + return { + __kind: type, + fields: [treasuryToBase(args)], + }; + } return { __kind: type, @@ -167,6 +174,16 @@ export function mapPlugin({ }; } + if (plug.__kind === 'Treasury') { + return { + [pluginKey]: { + authority, + offset, + ...treasuryFromBase(plug.fields[0]), + }, + }; + } + return { [pluginKey]: { authority, diff --git a/clients/js/src/plugins/treasury.ts b/clients/js/src/plugins/treasury.ts new file mode 100644 index 00000000..bf5044b4 --- /dev/null +++ b/clients/js/src/plugins/treasury.ts @@ -0,0 +1,20 @@ +import { lamports, SolAmount } from '@metaplex-foundation/umi'; +import { BaseTreasury } from '../generated'; + +export type Treasury = { + withdrawn: SolAmount; +}; + +export type TreasuryArgs = Treasury; + +export function treasuryToBase(s: Treasury): BaseTreasury { + return { + withdrawn: s.withdrawn.basisPoints, + }; +} + +export function treasuryFromBase(s: BaseTreasury): Treasury { + return { + withdrawn: lamports(s.withdrawn), + }; +} diff --git a/clients/js/src/plugins/types.ts b/clients/js/src/plugins/types.ts index bc5d4a1f..6c647923 100644 --- a/clients/js/src/plugins/types.ts +++ b/clients/js/src/plugins/types.ts @@ -25,10 +25,12 @@ import { VerifiedCreatorsArgs, Autograph, VerifiedCreators, + BaseTreasuryArgs, } from '../generated'; import { RoyaltiesArgs, RoyaltiesPlugin } from './royalties'; import { PluginAuthority } from './pluginAuthority'; import { MasterEdition, MasterEditionArgs } from './masterEdition'; +import { Treasury, TreasuryArgs } from './treasury'; // for backwards compatibility export { pluginAuthority, updateAuthority, ruleSet }; @@ -88,6 +90,10 @@ export type CreatePluginArgs = } | { type: 'AddBlocker'; + } + | { + type: 'Treasury'; + data: BaseTreasuryArgs; }; export type AuthorityArgsV2 = { @@ -143,7 +149,10 @@ export type AuthorityManagedPluginArgsV2 = } | ({ type: 'VerifiedCreators'; - } & VerifiedCreatorsArgs); + } & VerifiedCreatorsArgs) + | ({ + type: 'Treasury'; + } & TreasuryArgs); export type AssetAddablePluginArgsV2 = | OwnerManagedPluginArgsV2 @@ -181,6 +190,7 @@ export type AddBlockerPlugin = BasePlugin & AddBlocker; export type ImmutableMetadataPlugin = BasePlugin & ImmutableMetadata; export type VerifiedCreatorsPlugin = BasePlugin & VerifiedCreators; export type AutographPlugin = BasePlugin & Autograph; +export type TreasuryPlugin = BasePlugin & Treasury; export type CommonPluginsList = { attributes?: AttributesPlugin; @@ -204,6 +214,7 @@ export type AssetPluginsList = { export type CollectionPluginsList = { masterEdition?: MasterEditionPlugin; + treasury?: TreasuryPlugin; } & CommonPluginsList; export type PluginsList = AssetPluginsList & CollectionPluginsList; diff --git a/clients/js/test/plugins/collection/masterEdition.test.ts b/clients/js/test/plugins/collection/masterEdition.test.ts index 2544d47e..3fc193ba 100644 --- a/clients/js/test/plugins/collection/masterEdition.test.ts +++ b/clients/js/test/plugins/collection/masterEdition.test.ts @@ -128,7 +128,7 @@ test('it cannot add masterEdition to asset', async (t) => { }).sendAndConfirm(umi); await t.throwsAsync(result, { - name: 'InvalidPlugin', + name: 'PluginNotAllowedOnAsset', }); }); @@ -150,6 +150,6 @@ test('it cannot create asset with masterEdition', async (t) => { }); await t.throwsAsync(result, { - name: 'InvalidPlugin', + name: 'PluginNotAllowedOnAsset', }); }); diff --git a/clients/js/test/plugins/collection/treasury.test.ts b/clients/js/test/plugins/collection/treasury.test.ts new file mode 100644 index 00000000..f553dcc8 --- /dev/null +++ b/clients/js/test/plugins/collection/treasury.test.ts @@ -0,0 +1,348 @@ +import test from 'ava'; + +import { generateSigner, lamports } from '@metaplex-foundation/umi'; +import { + pluginAuthorityPair, + updatePluginAuthority, + createPlugin, + addPluginV1, + addCollectionPluginV1, + createCollectionV2, + pluginAuthorityPairV2, + updateCollectionPlugin, +} from '../../../src'; +import { + DEFAULT_COLLECTION, + assertCollection, + createAsset, + createCollection, + createUmi, +} from '../../_setupRaw'; + +test('it can add treasury to collection', async (t) => { + const umi = await createUmi(); + const collection = await createCollection(umi); + + await addCollectionPluginV1(umi, { + collection: collection.publicKey, + plugin: createPlugin({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + }), + }).sendAndConfirm(umi); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(0), + }, + }); +}); + +test('it can create collection with treasury', async (t) => { + const umi = await createUmi(); + + const collection = await createCollection(umi, { + plugins: [ + pluginAuthorityPair({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + authority: updatePluginAuthority(), + }), + ], + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(0), + }, + }); +}); + +test('it cannot create treasury with nonzero withdrawn', async (t) => { + const umi = await createUmi(); + + const result = createCollectionV2(umi, { + collection: generateSigner(umi), + name: 'TEST', + uri: 'www.test.com', + plugins: [ + pluginAuthorityPairV2({ + type: 'Treasury', + withdrawn: lamports(10), + }), + ], + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'InvalidTreasuryWithdrawn', + }); +}); + +test('it cannot add treasury with nonzero withdrawn', async (t) => { + const umi = await createUmi(); + const collection = await createCollection(umi); + + const result = addCollectionPluginV1(umi, { + collection: collection.publicKey, + plugin: createPlugin({ + type: 'Treasury', + data: { + withdrawn: 10, + }, + }), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'InvalidTreasuryWithdrawn', + }); +}); + +test('it cannot add treasury to asset', async (t) => { + const umi = await createUmi(); + + const asset = await createAsset(umi); + + const result = addPluginV1(umi, { + asset: asset.publicKey, + plugin: createPlugin({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + }), + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'PluginNotAllowedOnAsset', + }); +}); + +test('it cannot create asset with treasury', async (t) => { + const umi = await createUmi(); + + const result = createAsset(umi, { + plugins: [ + pluginAuthorityPair({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + authority: updatePluginAuthority(), + }), + ], + }); + + await t.throwsAsync(result, { + name: 'PluginNotAllowedOnAsset', + }); +}); + +test('it can withdraw SOL from treasury', async (t) => { + const umi = await createUmi(); + + const collection = await createCollection(umi, { + plugins: [ + pluginAuthorityPair({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + authority: updatePluginAuthority(), + }), + ], + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(0), + }, + }); + + await umi.rpc.airdrop(collection.publicKey, lamports(1_000_000)); + + const identityBeforeBalance = await umi.rpc.getBalance( + umi.identity.publicKey + ); + const collectionBeforeBalance = await umi.rpc.getBalance( + collection.publicKey + ); + + await updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: lamports(1_000_000), + }, + }).sendAndConfirm(umi); + + const identityAfterBalance = await umi.rpc.getBalance(umi.identity.publicKey); + const collectionAfterBalance = await umi.rpc.getBalance(collection.publicKey); + + const identityExpected = + identityBeforeBalance.basisPoints + + 1_000_000n - // Withdrawn + 5_000n; // Transaction fee + t.is(identityExpected, identityAfterBalance.basisPoints); + t.is( + collectionBeforeBalance.basisPoints - 1_000_000n, + collectionAfterBalance.basisPoints + ); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(1_000_000), + }, + }); +}); + +test('it cannot withdraw SOL from treasury if not the authority', async (t) => { + const umi = await createUmi(); + const authority = generateSigner(umi); + + const collection = await createCollection(umi, { + plugins: [ + pluginAuthorityPair({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + authority: updatePluginAuthority(), + }), + ], + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(0), + }, + }); + + await umi.rpc.airdrop(collection.publicKey, lamports(1_000_000)); + + const result = updateCollectionPlugin(umi, { + collection: collection.publicKey, + authority, + plugin: { + type: 'Treasury', + withdrawn: lamports(1_000_000), + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); +}); + +test('it cannot withdraw more than excess rent from treasury', async (t) => { + const umi = await createUmi(); + + const collection = await createCollection(umi, { + plugins: [ + pluginAuthorityPair({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + authority: updatePluginAuthority(), + }), + ], + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(0), + }, + }); + + await umi.rpc.airdrop(collection.publicKey, lamports(1_000_000)); + + const result = updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: lamports(1_000_001), + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'CannotOverdraw', + }); +}); + +test('it cannot withdraw entire balance from treasury', async (t) => { + const umi = await createUmi(); + + const collection = await createCollection(umi, { + plugins: [ + pluginAuthorityPair({ + type: 'Treasury', + data: { + withdrawn: 0, + }, + authority: updatePluginAuthority(), + }), + ], + }); + + const result = updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: collection.header.lamports, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'CannotOverdraw', + }); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + treasury: { + authority: { + type: 'UpdateAuthority', + }, + withdrawn: lamports(0), + }, + }); +}); diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 93e72b13..ecd28390 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -154,6 +154,18 @@ pub enum MplCoreError { /// 47 (0x2F) - Cannot add a Data Section without a linked external plugin #[error("Cannot add a Data Section without a linked external plugin")] CannotAddDataSection, + /// 48 (0x30) - Plugin is not allowed to be added to an Asset + #[error("Plugin is not allowed to be added to an Asset")] + PluginNotAllowedOnAsset, + /// 49 (0x31) - Plugin is not allowed to be added to a Collection + #[error("Plugin is not allowed to be added to a Collection")] + PluginNotAllowedOnCollection, + /// 50 (0x32) - Cannot add a treasury plugin with nonzero withdrawn amount + #[error("Cannot add a treasury plugin with nonzero withdrawn amount")] + InvalidTreasuryWithdrawn, + /// 51 (0x33) - Cannot withdraw more than excess rent from treasury + #[error("Cannot withdraw more than excess rent from treasury")] + CannotOverdraw, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/clients/rust/src/generated/types/mod.rs b/clients/rust/src/generated/types/mod.rs index 755acf21..7cf4e2bd 100644 --- a/clients/rust/src/generated/types/mod.rs +++ b/clients/rust/src/generated/types/mod.rs @@ -64,6 +64,7 @@ pub(crate) mod r#royalties; pub(crate) mod r#rule_set; pub(crate) mod r#seed; pub(crate) mod r#transfer_delegate; +pub(crate) mod r#treasury; pub(crate) mod r#update_authority; pub(crate) mod r#update_delegate; pub(crate) mod r#validation_result; @@ -130,6 +131,7 @@ pub use self::r#royalties::*; pub use self::r#rule_set::*; pub use self::r#seed::*; pub use self::r#transfer_delegate::*; +pub use self::r#treasury::*; pub use self::r#update_authority::*; pub use self::r#update_delegate::*; pub use self::r#validation_result::*; diff --git a/clients/rust/src/generated/types/plugin.rs b/clients/rust/src/generated/types/plugin.rs index 98a923a9..9f9a4057 100644 --- a/clients/rust/src/generated/types/plugin.rs +++ b/clients/rust/src/generated/types/plugin.rs @@ -18,6 +18,7 @@ use crate::generated::types::PermanentFreezeDelegate; use crate::generated::types::PermanentTransferDelegate; use crate::generated::types::Royalties; use crate::generated::types::TransferDelegate; +use crate::generated::types::Treasury; use crate::generated::types::UpdateDelegate; use crate::generated::types::VerifiedCreators; #[cfg(feature = "anchor")] @@ -45,4 +46,5 @@ pub enum Plugin { ImmutableMetadata(ImmutableMetadata), VerifiedCreators(VerifiedCreators), Autograph(Autograph), + Treasury(Treasury), } diff --git a/clients/rust/src/generated/types/plugin_type.rs b/clients/rust/src/generated/types/plugin_type.rs index 7eb36292..0346ca23 100644 --- a/clients/rust/src/generated/types/plugin_type.rs +++ b/clients/rust/src/generated/types/plugin_type.rs @@ -31,4 +31,5 @@ pub enum PluginType { ImmutableMetadata, VerifiedCreators, Autograph, + Treasury, } diff --git a/clients/rust/src/generated/types/treasury.rs b/clients/rust/src/generated/types/treasury.rs new file mode 100644 index 00000000..46ac8037 --- /dev/null +++ b/clients/rust/src/generated/types/treasury.rs @@ -0,0 +1,19 @@ +//! This code was AUTOGENERATED using the kinobi library. +//! Please DO NOT EDIT THIS FILE, instead use visitors +//! to add features, then rerun kinobi to update it. +//! +//! [https://github.com/metaplex-foundation/kinobi] +//! + +#[cfg(feature = "anchor")] +use anchor_lang::prelude::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize, BorshSerialize}; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(not(feature = "anchor"), derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Treasury { + pub withdrawn: u64, +} diff --git a/clients/rust/src/hooked/advanced_types.rs b/clients/rust/src/hooked/advanced_types.rs index 5dc81285..2fce64fe 100644 --- a/clients/rust/src/hooked/advanced_types.rs +++ b/clients/rust/src/hooked/advanced_types.rs @@ -12,7 +12,7 @@ use crate::{ ExternalCheckResult, ExternalPluginAdapter, ExternalPluginAdapterKey, FreezeDelegate, ImmutableMetadata, Key, LifecycleHook, LinkedAppData, LinkedLifecycleHook, MasterEdition, Oracle, PermanentBurnDelegate, PermanentFreezeDelegate, PermanentTransferDelegate, - PluginAuthority, Royalties, TransferDelegate, UpdateDelegate, VerifiedCreators, + PluginAuthority, Royalties, TransferDelegate, Treasury, UpdateDelegate, VerifiedCreators, }, }; @@ -160,6 +160,12 @@ pub struct AutographPlugin { pub autograph: Autograph, } +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct TreasuryPlugin { + pub base: BasePlugin, + pub treasury: Treasury, +} + #[derive(Debug, Default)] pub struct PluginsList { pub royalties: Option, @@ -177,6 +183,7 @@ pub struct PluginsList { pub immutable_metadata: Option, pub verified_creators: Option, pub autograph: Option, + pub treasury: Option, } #[derive(Debug, Default)] diff --git a/clients/rust/src/hooked/mod.rs b/clients/rust/src/hooked/mod.rs index 481dbbc4..9fddcf4c 100644 --- a/clients/rust/src/hooked/mod.rs +++ b/clients/rust/src/hooked/mod.rs @@ -49,6 +49,7 @@ impl From<&Plugin> for PluginType { Plugin::ImmutableMetadata(_) => PluginType::ImmutableMetadata, Plugin::VerifiedCreators(_) => PluginType::VerifiedCreators, Plugin::Autograph(_) => PluginType::Autograph, + Plugin::Treasury(_) => PluginType::Treasury, } } } diff --git a/clients/rust/src/hooked/plugin.rs b/clients/rust/src/hooked/plugin.rs index 9f815451..e9d206b3 100644 --- a/clients/rust/src/hooked/plugin.rs +++ b/clients/rust/src/hooked/plugin.rs @@ -21,6 +21,8 @@ use crate::{ TransferDelegatePlugin, UpdateDelegatePlugin, VerifiedCreatorsPlugin, }; +use super::TreasuryPlugin; + /// Fetch the plugin from the registry. pub fn fetch_plugin( account: &AccountInfo, @@ -345,6 +347,9 @@ pub(crate) fn registry_records_to_plugin_list( Plugin::Autograph(autograph) => { acc.autograph = Some(AutographPlugin { base, autograph }) } + Plugin::Treasury(treasury) => { + acc.treasury = Some(TreasuryPlugin { base, treasury }) + } } } Ok(acc) diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index e5bd5f64..eb1df99c 100755 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -279,7 +279,10 @@ kinobi.update( }, masterEdition: { name: "baseMasterEdition" - } + }, + treasury: { + name: "baseTreasury" + }, }) ) diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 85960483..c8b66ed3 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -3052,6 +3052,18 @@ "fields": [] } }, + { + "name": "Treasury", + "type": { + "kind": "struct", + "fields": [ + { + "name": "withdrawn", + "type": "u64" + } + ] + } + }, { "name": "UpdateDelegate", "type": { @@ -3897,6 +3909,14 @@ "defined": "Autograph" } ] + }, + { + "name": "Treasury", + "fields": [ + { + "defined": "Treasury" + } + ] } ] } @@ -3950,6 +3970,9 @@ }, { "name": "Autograph" + }, + { + "name": "Treasury" } ] } @@ -4852,6 +4875,26 @@ "code": 47, "name": "CannotAddDataSection", "msg": "Cannot add a Data Section without a linked external plugin" + }, + { + "code": 48, + "name": "PluginNotAllowedOnAsset", + "msg": "Plugin is not allowed to be added to an Asset" + }, + { + "code": 49, + "name": "PluginNotAllowedOnCollection", + "msg": "Plugin is not allowed to be added to a Collection" + }, + { + "code": 50, + "name": "InvalidTreasuryWithdrawn", + "msg": "Cannot add a treasury plugin with nonzero withdrawn amount" + }, + { + "code": 51, + "name": "CannotOverdraw", + "msg": "Cannot withdraw more than excess rent from treasury" } ], "metadata": { diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index 266684c2..03e6890d 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -200,6 +200,22 @@ pub enum MplCoreError { /// 47 - Cannot add a Data Section without a linked external plugin #[error("Cannot add a Data Section without a linked external plugin")] CannotAddDataSection, + + /// 48 - Plugin is not allowed to be added to an Asset + #[error("Plugin is not allowed to be added to an Asset")] + PluginNotAllowedOnAsset, + + /// 49 - Plugin is not allowed to be added to a Collection + #[error("Plugin is not allowed to be added to a Collection")] + PluginNotAllowedOnCollection, + + /// 50 - Cannot add a treasury plugin with nonzero withdrawn amount + #[error("Cannot add a treasury plugin with nonzero withdrawn amount")] + InvalidTreasuryWithdrawn, + + /// 51 - Cannot withdraw more than excess rent from treasury + #[error("Cannot withdraw more than excess rent from treasury")] + CannotOverdraw, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 2f47c5ed..12bcb186 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -135,6 +135,8 @@ impl PluginType { PluginType::UpdateDelegate => CheckResult::CanApprove, PluginType::Autograph => CheckResult::CanReject, PluginType::VerifiedCreators => CheckResult::CanReject, + PluginType::MasterEdition => CheckResult::CanReject, + PluginType::Treasury => CheckResult::CanReject, _ => CheckResult::None, } } @@ -245,6 +247,7 @@ impl Plugin { verified_creators.validate_add_plugin(ctx) } Plugin::Autograph(autograph) => autograph.validate_add_plugin(ctx), + Plugin::Treasury(treasury) => treasury.validate_add_plugin(ctx), } } @@ -286,6 +289,7 @@ impl Plugin { verified_creators.validate_remove_plugin(ctx) } Plugin::Autograph(autograph) => autograph.validate_remove_plugin(ctx), + Plugin::Treasury(treasury) => treasury.validate_remove_plugin(ctx), } } @@ -334,6 +338,7 @@ impl Plugin { verified_creators.validate_approve_plugin_authority(ctx) } Plugin::Autograph(autograph) => autograph.validate_approve_plugin_authority(ctx), + Plugin::Treasury(treasury) => treasury.validate_approve_plugin_authority(ctx), } } @@ -394,6 +399,7 @@ impl Plugin { verified_creators.validate_revoke_plugin_authority(ctx) } Plugin::Autograph(autograph) => autograph.validate_revoke_plugin_authority(ctx), + Plugin::Treasury(treasury) => treasury.validate_revoke_plugin_authority(ctx), }?; if result == ValidationResult::Pass { @@ -430,6 +436,7 @@ impl Plugin { } Plugin::VerifiedCreators(verified_creators) => verified_creators.validate_create(ctx), Plugin::Autograph(autograph) => autograph.validate_create(ctx), + Plugin::Treasury(treasury) => treasury.validate_create(ctx), } } @@ -460,6 +467,7 @@ impl Plugin { } Plugin::VerifiedCreators(verified_creators) => verified_creators.validate_update(ctx), Plugin::Autograph(autograph) => autograph.validate_update(ctx), + Plugin::Treasury(treasury) => treasury.validate_update(ctx), } } @@ -505,6 +513,7 @@ impl Plugin { verified_creators.validate_update_plugin(ctx) } Plugin::Autograph(autograph) => autograph.validate_update_plugin(ctx), + Plugin::Treasury(treasury) => treasury.validate_update_plugin(ctx), }?; match (&base_result, &result) { @@ -552,6 +561,7 @@ impl Plugin { Plugin::ImmutableMetadata(immutable_metadata) => immutable_metadata.validate_burn(ctx), Plugin::VerifiedCreators(verified_creators) => verified_creators.validate_burn(ctx), Plugin::Autograph(autograph) => autograph.validate_burn(ctx), + Plugin::Treasury(treasury) => treasury.validate_burn(ctx), } } @@ -582,6 +592,7 @@ impl Plugin { } Plugin::VerifiedCreators(verified_creators) => verified_creators.validate_transfer(ctx), Plugin::Autograph(autograph) => autograph.validate_transfer(ctx), + Plugin::Treasury(treasury) => treasury.validate_transfer(ctx), } } @@ -612,6 +623,7 @@ impl Plugin { } Plugin::VerifiedCreators(verified_creators) => verified_creators.validate_compress(ctx), Plugin::Autograph(autograph) => autograph.validate_compress(ctx), + Plugin::Treasury(treasury) => treasury.validate_compress(ctx), } } @@ -646,6 +658,7 @@ impl Plugin { verified_creators.validate_decompress(ctx) } Plugin::Autograph(autograph) => autograph.validate_decompress(ctx), + Plugin::Treasury(treasury) => treasury.validate_decompress(ctx), } } @@ -688,6 +701,7 @@ impl Plugin { verified_creators.validate_add_external_plugin_adapter(ctx) } Plugin::Autograph(autograph) => autograph.validate_add_external_plugin_adapter(ctx), + Plugin::Treasury(treasury) => treasury.validate_add_external_plugin_adapter(ctx), } } @@ -732,6 +746,7 @@ impl Plugin { verified_creators.validate_remove_external_plugin_adapter(ctx) } Plugin::Autograph(autograph) => autograph.validate_remove_external_plugin_adapter(ctx), + Plugin::Treasury(treasury) => treasury.validate_remove_external_plugin_adapter(ctx), } } } @@ -809,6 +824,7 @@ impl From for ValidationResult { /// The required context for a plugin validation. #[allow(dead_code)] +#[derive(Debug)] pub(crate) struct PluginValidationContext<'a, 'b> { /// This list of all the accounts passed into the instruction. pub accounts: &'a [AccountInfo<'a>], @@ -820,6 +836,8 @@ pub(crate) struct PluginValidationContext<'a, 'b> { pub self_authority: &'b Authority, /// The authority account info of ix `authority` signer pub authority_info: &'a AccountInfo<'a>, + /// The payer account of the ix + pub payer: &'a AccountInfo<'a>, /// The authorities types which match the authority signer pub resolved_authorities: Option<&'b [Authority]>, /// The new owner account for transfers @@ -828,7 +846,7 @@ pub(crate) struct PluginValidationContext<'a, 'b> { pub new_asset_authority: Option<&'b UpdateAuthority>, /// The new collection authority address. pub new_collection_authority: Option<&'b Pubkey>, - /// The plugin being acted upon with new data from the ix if any. This None for create. + /// The plugin being acted upon with new data from the ix if any. This is None for create. pub target_plugin: Option<&'b Plugin>, } @@ -965,10 +983,11 @@ pub(crate) fn validate_plugin_checks<'a>( accounts: &'a [AccountInfo<'a>], checks: &BTreeMap, authority: &'a AccountInfo<'a>, + payer: &'a AccountInfo<'a>, new_owner: Option<&'a AccountInfo<'a>>, new_asset_authority: Option<&UpdateAuthority>, new_collection_authority: Option<&Pubkey>, - new_plugin: Option<&Plugin>, + target_plugin: Option<&Plugin>, asset: Option<&'a AccountInfo<'a>>, collection: Option<&'a AccountInfo<'a>>, resolved_authorities: &[Authority], @@ -998,11 +1017,12 @@ pub(crate) fn validate_plugin_checks<'a>( collection_info: collection, self_authority: ®istry_record.authority, authority_info: authority, + payer, resolved_authorities: Some(resolved_authorities), new_owner, new_asset_authority, new_collection_authority, - target_plugin: new_plugin, + target_plugin, }; let result = plugin_validate_fp( @@ -1039,10 +1059,11 @@ pub(crate) fn validate_external_plugin_adapter_checks<'a>( (Key, ExternalCheckResultBits, ExternalRegistryRecord), >, authority: &'a AccountInfo<'a>, + payer: &'a AccountInfo<'a>, new_owner: Option<&'a AccountInfo<'a>>, new_asset_authority: Option<&UpdateAuthority>, new_collection_authority: Option<&Pubkey>, - new_plugin: Option<&Plugin>, + target_plugin: Option<&Plugin>, asset: Option<&'a AccountInfo<'a>>, collection: Option<&'a AccountInfo<'a>>, resolved_authorities: &[Authority], @@ -1070,11 +1091,12 @@ pub(crate) fn validate_external_plugin_adapter_checks<'a>( collection_info: collection, self_authority: &external_registry_record.authority, authority_info: authority, + payer, resolved_authorities: Some(resolved_authorities), new_owner, new_asset_authority, new_collection_authority, - target_plugin: new_plugin, + target_plugin, }; let result = external_plugin_adapter_validate_fp( diff --git a/programs/mpl-core/src/plugins/master_edition.rs b/programs/mpl-core/src/plugins/master_edition.rs index 74735125..d3ae84a5 100644 --- a/programs/mpl-core/src/plugins/master_edition.rs +++ b/programs/mpl-core/src/plugins/master_edition.rs @@ -1,6 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::program_error::ProgramError; -use super::PluginValidation; +use crate::error::MplCoreError; + +use super::{PluginType, PluginValidation, PluginValidationContext, ValidationResult}; /// The master edition plugin allows the creator to specify details on the master edition including max supply, name, and uri. /// The default authority for this plugin is the creator. @@ -15,4 +18,39 @@ pub struct MasterEdition { pub uri: Option, } -impl PluginValidation for MasterEdition {} +impl PluginValidation for MasterEdition { + fn validate_create( + &self, + ctx: &PluginValidationContext, + ) -> Result { + // Target plugin doesn't need to be populated for create, so we check if it exists, otherwise we pass. + if let Some(target_plugin) = ctx.target_plugin { + // You can't create the master edition plugin on an asset. + if PluginType::from(target_plugin) == PluginType::MasterEdition + && ctx.asset_info.is_some() + { + Err(MplCoreError::PluginNotAllowedOnAsset.into()) + } else { + Ok(ValidationResult::Pass) + } + } else { + Ok(ValidationResult::Pass) + } + } + + fn validate_add_plugin( + &self, + ctx: &PluginValidationContext, + ) -> Result { + // Target plugin must be populated for add_plugin. + let target_plugin = ctx.target_plugin.ok_or(MplCoreError::InvalidPlugin)?; + + // You can't add the master edition plugin to an asset. + if PluginType::from(target_plugin) == PluginType::MasterEdition && ctx.asset_info.is_some() + { + Err(MplCoreError::PluginNotAllowedOnAsset.into()) + } else { + Ok(ValidationResult::Pass) + } + } +} diff --git a/programs/mpl-core/src/plugins/mod.rs b/programs/mpl-core/src/plugins/mod.rs index b2177509..c23154e4 100644 --- a/programs/mpl-core/src/plugins/mod.rs +++ b/programs/mpl-core/src/plugins/mod.rs @@ -21,6 +21,7 @@ mod plugin_header; mod plugin_registry; mod royalties; mod transfer; +mod treasury; mod update_delegate; mod utils; mod verified_creators; @@ -48,6 +49,7 @@ pub use plugin_header::*; pub use plugin_registry::*; pub use royalties::*; pub use transfer::*; +pub use treasury::*; pub use update_delegate::*; pub use utils::*; pub use verified_creators::*; @@ -98,6 +100,8 @@ pub enum Plugin { VerifiedCreators(VerifiedCreators), /// Autograph plugin allows anybody to add their signature to the asset with an optional message Autograph(Autograph), + /// Treasury plugin allows for the Collection to contain a SOL treasury + Treasury(Treasury), } impl Plugin { @@ -172,6 +176,8 @@ pub enum PluginType { VerifiedCreators, /// Autograph plugin. Autograph, + /// Treasury plugin. + Treasury, } impl DataBlob for PluginType { @@ -202,6 +208,7 @@ impl From<&Plugin> for PluginType { Plugin::MasterEdition(_) => PluginType::MasterEdition, Plugin::VerifiedCreators(_) => PluginType::VerifiedCreators, Plugin::Autograph(_) => PluginType::Autograph, + Plugin::Treasury(_) => PluginType::Treasury, } } } @@ -225,6 +232,7 @@ impl PluginType { PluginType::MasterEdition => Authority::UpdateAuthority, PluginType::VerifiedCreators => Authority::UpdateAuthority, PluginType::Autograph => Authority::Owner, + PluginType::Treasury => Authority::UpdateAuthority, } } } diff --git a/programs/mpl-core/src/plugins/treasury.rs b/programs/mpl-core/src/plugins/treasury.rs new file mode 100644 index 00000000..f3a75fc4 --- /dev/null +++ b/programs/mpl-core/src/plugins/treasury.rs @@ -0,0 +1,103 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{program_error::ProgramError, rent::Rent, sysvar::Sysvar}; + +use crate::error::MplCoreError; + +use super::{ + abstain, Plugin, PluginType, PluginValidation, PluginValidationContext, ValidationResult, +}; + +/// The treasury plugin allows the creator to store a SOL treasury in the collection. +#[repr(C)] +#[derive(Clone, BorshSerialize, BorshDeserialize, Default, Debug, PartialEq, Eq)] +pub struct Treasury { + /// How much SOL has been withdrawn from the treasury, in lamports + pub withdrawn: u64, +} + +impl PluginValidation for Treasury { + fn validate_create( + &self, + ctx: &PluginValidationContext, + ) -> Result { + // Target plugin doesn't need to be populated for create, so we check if it exists, otherwise we pass. + if let Some(Plugin::Treasury(target_plugin)) = ctx.target_plugin { + // You can't create the treasury plugin on an asset. + if ctx.asset_info.is_some() { + Err(MplCoreError::PluginNotAllowedOnAsset.into()) + } + // You can't create a treasury plugin with nonzero withdrawn amount. + else if target_plugin.withdrawn > 0 { + Err(MplCoreError::InvalidTreasuryWithdrawn.into()) + } else { + abstain!() + } + } else { + abstain!() + } + } + + fn validate_add_plugin( + &self, + ctx: &PluginValidationContext, + ) -> Result { + // Target plugin must be populated for add_plugin. + let target_plugin = ctx.target_plugin.ok_or(MplCoreError::InvalidPlugin)?; + + // You can't add the treasury plugin to an asset. + if let Plugin::Treasury(target_plugin) = target_plugin { + if ctx.asset_info.is_some() { + Err(MplCoreError::PluginNotAllowedOnAsset.into()) + } + // You can't add a treasury plugin with nonzero withdrawn amount. + else if target_plugin.withdrawn > 0 { + Err(MplCoreError::InvalidTreasuryWithdrawn.into()) + } else { + abstain!() + } + } else { + abstain!() + } + } + + fn validate_update_plugin( + &self, + ctx: &PluginValidationContext, + ) -> Result { + if let Some(target_plugin) = ctx.target_plugin { + if PluginType::from(target_plugin) == PluginType::Treasury { + if let Plugin::Treasury(treasury) = target_plugin { + let collection = ctx.collection_info.ok_or(MplCoreError::MissingCollection)?; + // Withdrawing SOL from the treasury + if treasury.withdrawn > self.withdrawn { + let excess_rent = collection + .lamports() + .checked_sub(Rent::get()?.minimum_balance(collection.data_len())) + .ok_or(MplCoreError::NumericalOverflow)?; + let diff: u64 = treasury + .withdrawn + .checked_sub(self.withdrawn) + .ok_or(MplCoreError::NumericalOverflow)?; + + if diff > excess_rent { + return Err(MplCoreError::CannotOverdraw.into()); + } + + let auth_starting_lamports = ctx.payer.lamports(); + **ctx.payer.lamports.borrow_mut() = + auth_starting_lamports.checked_add(diff).unwrap(); + **collection.lamports.borrow_mut() = collection + .lamports() + .checked_sub(diff) + .ok_or(MplCoreError::NumericalOverflow)?; + } else { + return Err(MplCoreError::InvalidPluginOperation.into()); + } + } else { + return Err(MplCoreError::InvalidPlugin.into()); + } + } + } + abstain!() + } +} diff --git a/programs/mpl-core/src/processor/add_external_plugin_adapter.rs b/programs/mpl-core/src/processor/add_external_plugin_adapter.rs index 9f95acf6..92351697 100644 --- a/programs/mpl-core/src/processor/add_external_plugin_adapter.rs +++ b/programs/mpl-core/src/processor/add_external_plugin_adapter.rs @@ -68,6 +68,7 @@ pub(crate) fn add_external_plugin_adapter<'a>( collection_info: ctx.accounts.collection, self_authority: &Authority::UpdateAuthority, authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, @@ -89,6 +90,7 @@ pub(crate) fn add_external_plugin_adapter<'a>( let (mut asset, _, _) = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -154,6 +156,7 @@ pub(crate) fn add_collection_external_plugin_adapter<'a>( collection_info: Some(ctx.accounts.collection), self_authority: &Authority::UpdateAuthority, authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, @@ -175,6 +178,7 @@ pub(crate) fn add_collection_external_plugin_adapter<'a>( let (core, _, _) = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, None, diff --git a/programs/mpl-core/src/processor/add_plugin.rs b/programs/mpl-core/src/processor/add_plugin.rs index 77c1626b..d0a369f3 100644 --- a/programs/mpl-core/src/processor/add_plugin.rs +++ b/programs/mpl-core/src/processor/add_plugin.rs @@ -47,12 +47,6 @@ pub(crate) fn add_plugin<'a>( return Err(MplCoreError::NotAvailable.into()); } - // TODO move into plugin validation when asset/collection is part of validation context - let plugin_type = PluginType::from(&args.plugin); - if plugin_type == PluginType::MasterEdition { - return Err(MplCoreError::InvalidPlugin.into()); - } - // TODO: Seed with Rejected // TODO: refactor to allow add_plugin to approve additions let validation_ctx = PluginValidationContext { @@ -61,6 +55,7 @@ pub(crate) fn add_plugin<'a>( collection_info: ctx.accounts.collection, self_authority: &args.init_authority.unwrap_or(args.plugin.manager()), authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, @@ -75,6 +70,7 @@ pub(crate) fn add_plugin<'a>( let (mut asset, _, _) = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -136,6 +132,7 @@ pub(crate) fn add_collection_plugin<'a>( collection_info: Some(ctx.accounts.collection), self_authority: &args.init_authority.unwrap_or(args.plugin.manager()), authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, @@ -155,6 +152,7 @@ pub(crate) fn add_collection_plugin<'a>( let _ = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, Some(&args.plugin), diff --git a/programs/mpl-core/src/processor/approve_plugin_authority.rs b/programs/mpl-core/src/processor/approve_plugin_authority.rs index 1486b38a..38ce0de9 100644 --- a/programs/mpl-core/src/processor/approve_plugin_authority.rs +++ b/programs/mpl-core/src/processor/approve_plugin_authority.rs @@ -53,6 +53,7 @@ pub(crate) fn approve_plugin_authority<'a>( let (mut asset, _, _) = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -115,6 +116,7 @@ pub(crate) fn approve_collection_plugin_authority<'a>( let _ = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, Some(&plugin), diff --git a/programs/mpl-core/src/processor/burn.rs b/programs/mpl-core/src/processor/burn.rs index 0fad2e53..46ef9a73 100644 --- a/programs/mpl-core/src/processor/burn.rs +++ b/programs/mpl-core/src/processor/burn.rs @@ -86,6 +86,7 @@ pub(crate) fn burn<'a>(accounts: &'a [AccountInfo<'a>], args: BurnV1Args) -> Pro let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/compress.rs b/programs/mpl-core/src/processor/compress.rs index 893a93b3..4f5b0b59 100644 --- a/programs/mpl-core/src/processor/compress.rs +++ b/programs/mpl-core/src/processor/compress.rs @@ -46,6 +46,7 @@ pub(crate) fn compress<'a>( let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 52130b9b..f9dbf672 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -151,6 +151,7 @@ pub(crate) fn process_create<'a>( let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -180,11 +181,6 @@ pub(crate) fn process_create<'a>( ctx.accounts.system_program, )?; for plugin in &plugins { - // TODO move into plugin validation when asset/collection is part of validation context - let plugin_type = PluginType::from(&plugin.plugin); - if plugin_type == PluginType::MasterEdition { - return Err(MplCoreError::InvalidPlugin.into()); - } if PluginType::check_create(&PluginType::from(&plugin.plugin)) != CheckResult::None { @@ -194,11 +190,12 @@ pub(crate) fn process_create<'a>( collection_info: ctx.accounts.collection, self_authority: &plugin.authority.unwrap_or(plugin.plugin.manager()), authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, new_collection_authority: None, - target_plugin: None, + target_plugin: Some(&plugin.plugin), }; match Plugin::validate_create(&plugin.plugin, &validation_ctx)? { ValidationResult::Rejected => approved = false, @@ -251,6 +248,7 @@ pub(crate) fn process_create<'a>( // External plugin adapters are always managed by the update authority. self_authority: &Authority::UpdateAuthority, authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, diff --git a/programs/mpl-core/src/processor/create_collection.rs b/programs/mpl-core/src/processor/create_collection.rs index 33d9a732..8424f9fa 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -138,11 +138,12 @@ pub(crate) fn process_create_collection<'a>( collection_info: Some(ctx.accounts.collection), self_authority: &plugin.authority.unwrap_or(plugin.plugin.manager()), authority_info: ctx.accounts.payer, + payer: ctx.accounts.payer, resolved_authorities: None, new_owner: None, new_asset_authority: None, new_collection_authority: None, - target_plugin: None, + target_plugin: Some(&plugin.plugin), }; match Plugin::validate_create(&plugin.plugin, &validation_ctx)? { ValidationResult::Rejected => approved = false, diff --git a/programs/mpl-core/src/processor/decompress.rs b/programs/mpl-core/src/processor/decompress.rs index a9bd17df..44ca3a5b 100644 --- a/programs/mpl-core/src/processor/decompress.rs +++ b/programs/mpl-core/src/processor/decompress.rs @@ -62,6 +62,7 @@ pub(crate) fn decompress<'a>( let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, diff --git a/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs b/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs index f5f5f253..b92e1b7d 100644 --- a/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs +++ b/programs/mpl-core/src/processor/remove_external_plugin_adapter.rs @@ -67,6 +67,7 @@ pub(crate) fn remove_external_plugin_adapter<'a>( let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -137,6 +138,7 @@ pub(crate) fn remove_collection_external_plugin_adapter<'a>( let _ = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, None, diff --git a/programs/mpl-core/src/processor/remove_plugin.rs b/programs/mpl-core/src/processor/remove_plugin.rs index 83efdd9c..7f711ea8 100644 --- a/programs/mpl-core/src/processor/remove_plugin.rs +++ b/programs/mpl-core/src/processor/remove_plugin.rs @@ -59,6 +59,7 @@ pub(crate) fn remove_plugin<'a>( let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -131,6 +132,7 @@ pub(crate) fn remove_collection_plugin<'a>( let _ = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, Some(&plugin_to_remove), diff --git a/programs/mpl-core/src/processor/revoke_plugin_authority.rs b/programs/mpl-core/src/processor/revoke_plugin_authority.rs index f68cf57c..5c2f7f74 100644 --- a/programs/mpl-core/src/processor/revoke_plugin_authority.rs +++ b/programs/mpl-core/src/processor/revoke_plugin_authority.rs @@ -60,6 +60,7 @@ pub(crate) fn revoke_plugin_authority<'a>( let _ = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -136,6 +137,7 @@ pub(crate) fn revoke_collection_plugin_authority<'a>( let _ = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, Some(&plugin), diff --git a/programs/mpl-core/src/processor/transfer.rs b/programs/mpl-core/src/processor/transfer.rs index d6c5f550..3a31c90e 100644 --- a/programs/mpl-core/src/processor/transfer.rs +++ b/programs/mpl-core/src/processor/transfer.rs @@ -79,6 +79,7 @@ pub(crate) fn transfer<'a>(accounts: &'a [AccountInfo<'a>], args: TransferV1Args let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, Some(ctx.accounts.new_owner), diff --git a/programs/mpl-core/src/processor/update.rs b/programs/mpl-core/src/processor/update.rs index e51f3d38..7da0b547 100644 --- a/programs/mpl-core/src/processor/update.rs +++ b/programs/mpl-core/src/processor/update.rs @@ -109,6 +109,7 @@ fn update<'a>( let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -271,6 +272,7 @@ pub(crate) fn update_collection<'a>( let (mut collection, plugin_header, plugin_registry) = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, ctx.accounts.new_update_authority.map(|a| a.key), None, 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 ed6d8622..20ce4731 100644 --- a/programs/mpl-core/src/processor/update_external_plugin_adapter.rs +++ b/programs/mpl-core/src/processor/update_external_plugin_adapter.rs @@ -69,6 +69,7 @@ pub(crate) fn update_external_plugin_adapter<'a>( collection_info: ctx.accounts.collection, self_authority: &external_registry_record.authority, authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: Some(&resolved_authorities), new_owner: None, new_asset_authority: None, @@ -147,6 +148,7 @@ pub(crate) fn update_collection_external_plugin_adapter<'a>( collection_info: Some(ctx.accounts.collection), self_authority: &external_registry_record.authority, authority_info: authority, + payer: ctx.accounts.payer, resolved_authorities: Some(&resolved_authorities), new_owner: None, new_asset_authority: None, diff --git a/programs/mpl-core/src/processor/update_plugin.rs b/programs/mpl-core/src/processor/update_plugin.rs index a4810f36..5f5a4b84 100644 --- a/programs/mpl-core/src/processor/update_plugin.rs +++ b/programs/mpl-core/src/processor/update_plugin.rs @@ -50,6 +50,7 @@ pub(crate) fn update_plugin<'a>( let (mut asset, plugin_header, plugin_registry) = validate_asset_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.asset, ctx.accounts.collection, None, @@ -111,6 +112,7 @@ pub(crate) fn update_collection_plugin<'a>( let (collection, plugin_header, plugin_registry) = validate_collection_permissions( accounts, authority, + ctx.accounts.payer, ctx.accounts.collection, None, Some(&args.plugin), diff --git a/programs/mpl-core/src/utils.rs b/programs/mpl-core/src/utils.rs index 340b6dea..883ecf1a 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -204,11 +204,12 @@ pub(crate) fn resize_or_reallocate_account<'a>( pub(crate) fn validate_asset_permissions<'a>( accounts: &'a [AccountInfo<'a>], authority_info: &'a AccountInfo<'a>, + payer: &'a AccountInfo<'a>, asset: &'a AccountInfo<'a>, collection: Option<&'a AccountInfo<'a>>, new_owner: Option<&'a AccountInfo<'a>>, new_authority: Option<&UpdateAuthority>, - new_plugin: Option<&Plugin>, + target_plugin: Option<&Plugin>, new_external_plugin_adapter: Option<&ExternalPluginAdapter>, asset_check_fp: fn() -> CheckResult, collection_check_fp: fn() -> CheckResult, @@ -311,7 +312,7 @@ pub(crate) fn validate_asset_permissions<'a>( match asset_validate_fp( &deserialized_asset, authority_info, - new_plugin, + target_plugin, new_external_plugin_adapter, )? { ValidationResult::Approved => approved = true, @@ -327,7 +328,7 @@ pub(crate) fn validate_asset_permissions<'a>( match collection_validate_fp( &CollectionV1::load(collection.ok_or(MplCoreError::MissingCollection)?, 0)?, authority_info, - new_plugin, + target_plugin, new_external_plugin_adapter, )? { ValidationResult::Approved => approved = true, @@ -344,10 +345,11 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &checks, authority_info, + payer, new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -366,10 +368,11 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &checks, authority_info, + payer, new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -389,10 +392,11 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &external_checks, authority_info, + payer, new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -410,10 +414,11 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &external_checks, authority_info, + payer, new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -441,9 +446,10 @@ pub(crate) fn validate_asset_permissions<'a>( pub(crate) fn validate_collection_permissions<'a>( accounts: &'a [AccountInfo<'a>], authority_info: &'a AccountInfo<'a>, + payer: &'a AccountInfo<'a>, collection: &'a AccountInfo<'a>, new_authority: Option<&Pubkey>, - new_plugin: Option<&Plugin>, + target_plugin: Option<&Plugin>, new_external_plugin_adapter: Option<&ExternalPluginAdapter>, collection_check_fp: fn() -> CheckResult, plugin_check_fp: fn(&PluginType) -> CheckResult, @@ -517,7 +523,7 @@ pub(crate) fn validate_collection_permissions<'a>( Key::CollectionV1 => collection_validate_fp( &deserialized_collection, authority_info, - new_plugin, + target_plugin, new_external_plugin_adapter, )?, _ => return Err(MplCoreError::IncorrectAccount.into()), @@ -537,10 +543,11 @@ pub(crate) fn validate_collection_permissions<'a>( accounts, &checks, authority_info, + payer, None, None, new_authority, - new_plugin, + target_plugin, None, Some(collection), &resolved_authorities, @@ -560,10 +567,11 @@ pub(crate) fn validate_collection_permissions<'a>( accounts, &external_checks, authority_info, + payer, None, None, new_authority, - new_plugin, + target_plugin, None, Some(collection), &resolved_authorities,