From 809c01c7aa9459469221491f0331072d052e9893 Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Fri, 16 Aug 2024 16:18:04 -0400 Subject: [PATCH 1/6] Moving master edition asset checks to validation. --- clients/js/src/generated/errors/mplCore.ts | 29 +++++++++++++ .../plugins/collection/masterEdition.test.ts | 4 +- clients/rust/src/generated/errors/mpl_core.rs | 6 +++ idls/mpl_core.json | 10 +++++ programs/mpl-core/src/error.rs | 8 ++++ programs/mpl-core/src/plugins/lifecycle.rs | 12 +++--- .../mpl-core/src/plugins/master_edition.rs | 42 ++++++++++++++++++- programs/mpl-core/src/processor/add_plugin.rs | 6 --- programs/mpl-core/src/processor/create.rs | 7 +--- .../src/processor/create_collection.rs | 2 +- programs/mpl-core/src/utils.rs | 22 +++++----- 11 files changed, 115 insertions(+), 33 deletions(-) diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 0c7414a4..d7894152 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -689,6 +689,35 @@ 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 +); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors 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/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index 93e72b13..d6f96f3b 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -154,6 +154,12 @@ 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, } impl solana_program::program_error::PrintProgramError for MplCoreError { diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 85960483..fd46057c 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -4852,6 +4852,16 @@ "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" } ], "metadata": { diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index 266684c2..43fa5d5d 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -200,6 +200,14 @@ 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, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 2f47c5ed..5758f741 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -135,6 +135,7 @@ impl PluginType { PluginType::UpdateDelegate => CheckResult::CanApprove, PluginType::Autograph => CheckResult::CanReject, PluginType::VerifiedCreators => CheckResult::CanReject, + PluginType::MasterEdition => CheckResult::CanReject, _ => CheckResult::None, } } @@ -809,6 +810,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>], @@ -828,7 +830,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>, } @@ -968,7 +970,7 @@ pub(crate) fn validate_plugin_checks<'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], @@ -1002,7 +1004,7 @@ pub(crate) fn validate_plugin_checks<'a>( new_owner, new_asset_authority, new_collection_authority, - target_plugin: new_plugin, + target_plugin, }; let result = plugin_validate_fp( @@ -1042,7 +1044,7 @@ pub(crate) fn validate_external_plugin_adapter_checks<'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], @@ -1074,7 +1076,7 @@ pub(crate) fn validate_external_plugin_adapter_checks<'a>( 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..60ce8896 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, utils::load_key}; + +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/processor/add_plugin.rs b/programs/mpl-core/src/processor/add_plugin.rs index 77c1626b..790ba313 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 { diff --git a/programs/mpl-core/src/processor/create.rs b/programs/mpl-core/src/processor/create.rs index 52130b9b..a40d4ff1 100644 --- a/programs/mpl-core/src/processor/create.rs +++ b/programs/mpl-core/src/processor/create.rs @@ -180,11 +180,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 { @@ -198,7 +193,7 @@ pub(crate) fn process_create<'a>( 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/create_collection.rs b/programs/mpl-core/src/processor/create_collection.rs index 33d9a732..4b886d8d 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -142,7 +142,7 @@ pub(crate) fn process_create_collection<'a>( 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/utils.rs b/programs/mpl-core/src/utils.rs index 340b6dea..915dc319 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -208,7 +208,7 @@ pub(crate) fn validate_asset_permissions<'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 +311,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 +327,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, @@ -347,7 +347,7 @@ pub(crate) fn validate_asset_permissions<'a>( new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -369,7 +369,7 @@ pub(crate) fn validate_asset_permissions<'a>( new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -392,7 +392,7 @@ pub(crate) fn validate_asset_permissions<'a>( new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -413,7 +413,7 @@ pub(crate) fn validate_asset_permissions<'a>( new_owner, new_authority, None, - new_plugin, + target_plugin, Some(asset), collection, &resolved_authorities, @@ -443,7 +443,7 @@ pub(crate) fn validate_collection_permissions<'a>( authority_info: &'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 +517,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()), @@ -540,7 +540,7 @@ pub(crate) fn validate_collection_permissions<'a>( None, None, new_authority, - new_plugin, + target_plugin, None, Some(collection), &resolved_authorities, @@ -563,7 +563,7 @@ pub(crate) fn validate_collection_permissions<'a>( None, None, new_authority, - new_plugin, + target_plugin, None, Some(collection), &resolved_authorities, From da67dde372c0fa82a203708ecacc39c23ce3b97d Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Sat, 17 Aug 2024 15:47:36 -0400 Subject: [PATCH 2/6] Adding treasury plugin --- clients/js/src/generated/errors/mplCore.ts | 34 ++ .../js/src/generated/types/baseTreasury.ts | 22 + clients/js/src/generated/types/index.ts | 1 + clients/js/src/generated/types/plugin.ts | 19 +- clients/js/src/generated/types/pluginType.ts | 1 + clients/js/src/plugins/lib.ts | 17 + clients/js/src/plugins/treasury.ts | 19 + clients/js/src/plugins/types.ts | 13 +- .../test/plugins/collection/treasury.test.ts | 386 ++++++++++++++++++ clients/rust/src/generated/errors/mpl_core.rs | 6 + clients/rust/src/generated/types/mod.rs | 2 + clients/rust/src/generated/types/plugin.rs | 2 + .../rust/src/generated/types/plugin_type.rs | 1 + clients/rust/src/generated/types/treasury.rs | 19 + configs/kinobi.cjs | 5 +- idls/mpl_core.json | 33 ++ programs/mpl-core/src/error.rs | 8 + programs/mpl-core/src/plugins/lifecycle.rs | 20 + .../mpl-core/src/plugins/master_edition.rs | 2 +- programs/mpl-core/src/plugins/mod.rs | 8 + programs/mpl-core/src/plugins/treasury.rs | 125 ++++++ .../processor/add_external_plugin_adapter.rs | 4 + programs/mpl-core/src/processor/add_plugin.rs | 4 + .../src/processor/approve_plugin_authority.rs | 2 + programs/mpl-core/src/processor/burn.rs | 1 + programs/mpl-core/src/processor/compress.rs | 1 + programs/mpl-core/src/processor/create.rs | 3 + .../src/processor/create_collection.rs | 1 + programs/mpl-core/src/processor/decompress.rs | 1 + .../remove_external_plugin_adapter.rs | 2 + .../mpl-core/src/processor/remove_plugin.rs | 2 + .../src/processor/revoke_plugin_authority.rs | 2 + programs/mpl-core/src/processor/transfer.rs | 1 + programs/mpl-core/src/processor/update.rs | 2 + .../update_external_plugin_adapter.rs | 2 + .../mpl-core/src/processor/update_plugin.rs | 2 + programs/mpl-core/src/utils.rs | 8 + 37 files changed, 776 insertions(+), 5 deletions(-) create mode 100644 clients/js/src/generated/types/baseTreasury.ts create mode 100644 clients/js/src/plugins/treasury.ts create mode 100644 clients/js/test/plugins/collection/treasury.test.ts create mode 100644 clients/rust/src/generated/types/treasury.rs create mode 100644 programs/mpl-core/src/plugins/treasury.rs diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index d7894152..4254682a 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -718,6 +718,40 @@ nameToErrorMap.set( 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..32603dc3 --- /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, i64, struct } 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', i64()]], { + 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..7ec41994 --- /dev/null +++ b/clients/js/src/plugins/treasury.ts @@ -0,0 +1,19 @@ +import { BaseTreasury } from '../generated'; + +export type Treasury = { + withdrawn: number; +}; + +export type TreasuryArgs = Treasury; + +export function treasuryToBase(s: Treasury): BaseTreasury { + return { + withdrawn: BigInt(s.withdrawn), + }; +} + +export function treasuryFromBase(s: BaseTreasury): Treasury { + return { + withdrawn: Number(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/treasury.test.ts b/clients/js/test/plugins/collection/treasury.test.ts new file mode 100644 index 00000000..e43367b5 --- /dev/null +++ b/clients/js/test/plugins/collection/treasury.test.ts @@ -0,0 +1,386 @@ +import test from 'ava'; + +import { generateSigner } 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: 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: 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: 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 deposit SOL to 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: 0, + }, + }); + + 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: -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 - // Deposited + 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: -1_000_000, + }, + }); +}); + +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: 0, + }, + }); + + await updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: -1_000_000, + }, + }).sendAndConfirm(umi); + + 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: 0, + }, + }).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: 0, + }, + }); +}); + +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: 0, + }, + }); + + await updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: -1_000_000, + }, + }).sendAndConfirm(umi); + + const result = updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: 1, + }, + }).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: Number(collection.header.lamports.basisPoints), + }, + }).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: 0, + }, + }); +}); diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index d6f96f3b..ecd28390 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -160,6 +160,12 @@ pub enum MplCoreError { /// 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..db2fbce7 --- /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: i64, +} 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 fd46057c..9fe74884 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": "i64" + } + ] + } + }, { "name": "UpdateDelegate", "type": { @@ -3897,6 +3909,14 @@ "defined": "Autograph" } ] + }, + { + "name": "Treasury", + "fields": [ + { + "defined": "Treasury" + } + ] } ] } @@ -3950,6 +3970,9 @@ }, { "name": "Autograph" + }, + { + "name": "Treasury" } ] } @@ -4862,6 +4885,16 @@ "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 43fa5d5d..03e6890d 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -208,6 +208,14 @@ pub enum MplCoreError { /// 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 5758f741..12bcb186 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -136,6 +136,7 @@ impl PluginType { PluginType::Autograph => CheckResult::CanReject, PluginType::VerifiedCreators => CheckResult::CanReject, PluginType::MasterEdition => CheckResult::CanReject, + PluginType::Treasury => CheckResult::CanReject, _ => CheckResult::None, } } @@ -246,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), } } @@ -287,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), } } @@ -335,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), } } @@ -395,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 { @@ -431,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), } } @@ -461,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), } } @@ -506,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) { @@ -553,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), } } @@ -583,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), } } @@ -613,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), } } @@ -647,6 +658,7 @@ impl Plugin { verified_creators.validate_decompress(ctx) } Plugin::Autograph(autograph) => autograph.validate_decompress(ctx), + Plugin::Treasury(treasury) => treasury.validate_decompress(ctx), } } @@ -689,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), } } @@ -733,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), } } } @@ -822,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 @@ -967,6 +983,7 @@ 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>, @@ -1000,6 +1017,7 @@ 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, @@ -1041,6 +1059,7 @@ 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>, @@ -1072,6 +1091,7 @@ 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, diff --git a/programs/mpl-core/src/plugins/master_edition.rs b/programs/mpl-core/src/plugins/master_edition.rs index 60ce8896..d3ae84a5 100644 --- a/programs/mpl-core/src/plugins/master_edition.rs +++ b/programs/mpl-core/src/plugins/master_edition.rs @@ -1,7 +1,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::program_error::ProgramError; -use crate::{error::MplCoreError, utils::load_key}; +use crate::error::MplCoreError; use super::{PluginType, PluginValidation, PluginValidationContext, ValidationResult}; 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..363fd0b9 --- /dev/null +++ b/programs/mpl-core/src/plugins/treasury.rs @@ -0,0 +1,125 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + program::invoke, program_error::ProgramError, rent::Rent, system_instruction, 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: i64, +} + +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 { + Ok(ValidationResult::Pass) + } + } 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)?; + solana_program::msg!("Current Withdrawn: {:?}", treasury.withdrawn); + solana_program::msg!("New Withdrawn: {:?}", self.withdrawn); + match treasury.withdrawn.cmp(&self.withdrawn) { + // Depositing SOL into the treasury + std::cmp::Ordering::Less => { + solana_program::msg!("Treasury: Depositing SOL into the treasury"); + let diff: u64 = self + .withdrawn + .checked_sub(treasury.withdrawn) + .ok_or(MplCoreError::NumericalOverflow)? + .try_into() + .map_err(|_| MplCoreError::NumericalOverflow)?; + invoke( + &system_instruction::transfer(ctx.payer.key, collection.key, diff), + &[ctx.payer.clone(), collection.clone()], + )?; + } + // Withdrawing SOL from the treasury + std::cmp::Ordering::Greater => { + solana_program::msg!("Treasury: Withdrawing SOL from the treasury"); + 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)? + .try_into() + .map_err(|_| 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)?; + } + std::cmp::Ordering::Equal => {} + } + } else { + return Err(MplCoreError::InvalidPlugin.into()); + } + } + } + Ok(ValidationResult::Pass) + } +} 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 790ba313..d0a369f3 100644 --- a/programs/mpl-core/src/processor/add_plugin.rs +++ b/programs/mpl-core/src/processor/add_plugin.rs @@ -55,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, @@ -69,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, @@ -130,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, @@ -149,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 a40d4ff1..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, @@ -189,6 +190,7 @@ 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, @@ -246,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 4b886d8d..8424f9fa 100644 --- a/programs/mpl-core/src/processor/create_collection.rs +++ b/programs/mpl-core/src/processor/create_collection.rs @@ -138,6 +138,7 @@ 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, 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 915dc319..883ecf1a 100644 --- a/programs/mpl-core/src/utils.rs +++ b/programs/mpl-core/src/utils.rs @@ -204,6 +204,7 @@ 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>>, @@ -344,6 +345,7 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &checks, authority_info, + payer, new_owner, new_authority, None, @@ -366,6 +368,7 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &checks, authority_info, + payer, new_owner, new_authority, None, @@ -389,6 +392,7 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &external_checks, authority_info, + payer, new_owner, new_authority, None, @@ -410,6 +414,7 @@ pub(crate) fn validate_asset_permissions<'a>( accounts, &external_checks, authority_info, + payer, new_owner, new_authority, None, @@ -441,6 +446,7 @@ 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>, target_plugin: Option<&Plugin>, @@ -537,6 +543,7 @@ pub(crate) fn validate_collection_permissions<'a>( accounts, &checks, authority_info, + payer, None, None, new_authority, @@ -560,6 +567,7 @@ pub(crate) fn validate_collection_permissions<'a>( accounts, &external_checks, authority_info, + payer, None, None, new_authority, From 2cc6cc55f31290f9a72013327eb9487a94ba1adf Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Sat, 17 Aug 2024 16:25:17 -0400 Subject: [PATCH 3/6] Adding another test. --- .../test/plugins/collection/treasury.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/clients/js/test/plugins/collection/treasury.test.ts b/clients/js/test/plugins/collection/treasury.test.ts index e43367b5..3e595fad 100644 --- a/clients/js/test/plugins/collection/treasury.test.ts +++ b/clients/js/test/plugins/collection/treasury.test.ts @@ -297,6 +297,54 @@ test('it can withdraw SOL from treasury', async (t) => { }); }); +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: 0, + }, + }); + + await updateCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'Treasury', + withdrawn: -1_000_000, + }, + }).sendAndConfirm(umi); + + const result = updateCollectionPlugin(umi, { + collection: collection.publicKey, + authority, + plugin: { + type: 'Treasury', + withdrawn: 0, + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); +}); + test('it cannot withdraw more than excess rent from treasury', async (t) => { const umi = await createUmi(); From 226e8692a7e6873b49c23ca55298d5d5e7ce697d Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Sat, 17 Aug 2024 20:12:38 -0400 Subject: [PATCH 4/6] Removing overcomplicated withdraw. --- .../test/plugins/collection/treasury.test.ts | 102 ++---------------- programs/mpl-core/src/plugins/treasury.rs | 70 +++++------- 2 files changed, 34 insertions(+), 138 deletions(-) diff --git a/clients/js/test/plugins/collection/treasury.test.ts b/clients/js/test/plugins/collection/treasury.test.ts index 3e595fad..a1f05957 100644 --- a/clients/js/test/plugins/collection/treasury.test.ts +++ b/clients/js/test/plugins/collection/treasury.test.ts @@ -1,6 +1,6 @@ import test from 'ava'; -import { generateSigner } from '@metaplex-foundation/umi'; +import { generateSigner, lamports } from '@metaplex-foundation/umi'; import { pluginAuthorityPair, updatePluginAuthority, @@ -153,74 +153,6 @@ test('it cannot create asset with treasury', async (t) => { }); }); -test('it can deposit SOL to 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: 0, - }, - }); - - 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: -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 - // Deposited - 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: -1_000_000, - }, - }); -}); - test('it can withdraw SOL from treasury', async (t) => { const umi = await createUmi(); @@ -248,13 +180,7 @@ test('it can withdraw SOL from treasury', async (t) => { }, }); - await updateCollectionPlugin(umi, { - collection: collection.publicKey, - plugin: { - type: 'Treasury', - withdrawn: -1_000_000, - }, - }).sendAndConfirm(umi); + await umi.rpc.airdrop(collection.publicKey, lamports(1_000_000)); const identityBeforeBalance = await umi.rpc.getBalance( umi.identity.publicKey @@ -267,7 +193,7 @@ test('it can withdraw SOL from treasury', async (t) => { collection: collection.publicKey, plugin: { type: 'Treasury', - withdrawn: 0, + withdrawn: 1_000_000, }, }).sendAndConfirm(umi); @@ -292,7 +218,7 @@ test('it can withdraw SOL from treasury', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: 1_000_000, }, }); }); @@ -325,20 +251,14 @@ test('it cannot withdraw SOL from treasury if not the authority', async (t) => { }, }); - await updateCollectionPlugin(umi, { - collection: collection.publicKey, - plugin: { - type: 'Treasury', - withdrawn: -1_000_000, - }, - }).sendAndConfirm(umi); + await umi.rpc.airdrop(collection.publicKey, lamports(1_000_000)); const result = updateCollectionPlugin(umi, { collection: collection.publicKey, authority, plugin: { type: 'Treasury', - withdrawn: 0, + withdrawn: 1_000_000, }, }).sendAndConfirm(umi); @@ -372,19 +292,13 @@ test('it cannot withdraw more than excess rent from treasury', async (t) => { }, }); - await updateCollectionPlugin(umi, { - collection: collection.publicKey, - plugin: { - type: 'Treasury', - withdrawn: -1_000_000, - }, - }).sendAndConfirm(umi); + await umi.rpc.airdrop(collection.publicKey, lamports(1_000_000)); const result = updateCollectionPlugin(umi, { collection: collection.publicKey, plugin: { type: 'Treasury', - withdrawn: 1, + withdrawn: 1_000_001, }, }).sendAndConfirm(umi); diff --git a/programs/mpl-core/src/plugins/treasury.rs b/programs/mpl-core/src/plugins/treasury.rs index 363fd0b9..ced1d9cf 100644 --- a/programs/mpl-core/src/plugins/treasury.rs +++ b/programs/mpl-core/src/plugins/treasury.rs @@ -32,7 +32,7 @@ impl PluginValidation for Treasury { else if target_plugin.withdrawn > 0 { Err(MplCoreError::InvalidTreasuryWithdrawn.into()) } else { - Ok(ValidationResult::Pass) + abstain!() } } else { abstain!() @@ -70,56 +70,38 @@ impl PluginValidation for Treasury { if PluginType::from(target_plugin) == PluginType::Treasury { if let Plugin::Treasury(treasury) = target_plugin { let collection = ctx.collection_info.ok_or(MplCoreError::MissingCollection)?; - solana_program::msg!("Current Withdrawn: {:?}", treasury.withdrawn); - solana_program::msg!("New Withdrawn: {:?}", self.withdrawn); - match treasury.withdrawn.cmp(&self.withdrawn) { - // Depositing SOL into the treasury - std::cmp::Ordering::Less => { - solana_program::msg!("Treasury: Depositing SOL into the treasury"); - let diff: u64 = self - .withdrawn - .checked_sub(treasury.withdrawn) - .ok_or(MplCoreError::NumericalOverflow)? - .try_into() - .map_err(|_| MplCoreError::NumericalOverflow)?; - invoke( - &system_instruction::transfer(ctx.payer.key, collection.key, diff), - &[ctx.payer.clone(), collection.clone()], - )?; - } - // Withdrawing SOL from the treasury - std::cmp::Ordering::Greater => { - solana_program::msg!("Treasury: Withdrawing SOL from the treasury"); - 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)? - .try_into() - .map_err(|_| MplCoreError::NumericalOverflow)?; + // 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)? + .try_into() + .map_err(|_| 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)?; + if diff > excess_rent { + return Err(MplCoreError::CannotOverdraw.into()); } - std::cmp::Ordering::Equal => {} + + 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()); } } } - Ok(ValidationResult::Pass) + abstain!() } } From 17b79cbedf9f90aae9562e3f04f7aadebccea51b Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Sat, 17 Aug 2024 20:21:55 -0400 Subject: [PATCH 5/6] More usable typing and fixing rust. --- clients/js/src/plugins/treasury.ts | 7 +++--- .../test/plugins/collection/treasury.test.ts | 24 +++++++++---------- clients/rust/src/hooked/advanced_types.rs | 9 ++++++- clients/rust/src/hooked/mod.rs | 1 + clients/rust/src/hooked/plugin.rs | 5 ++++ programs/mpl-core/src/plugins/treasury.rs | 4 +--- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/clients/js/src/plugins/treasury.ts b/clients/js/src/plugins/treasury.ts index 7ec41994..bf5044b4 100644 --- a/clients/js/src/plugins/treasury.ts +++ b/clients/js/src/plugins/treasury.ts @@ -1,19 +1,20 @@ +import { lamports, SolAmount } from '@metaplex-foundation/umi'; import { BaseTreasury } from '../generated'; export type Treasury = { - withdrawn: number; + withdrawn: SolAmount; }; export type TreasuryArgs = Treasury; export function treasuryToBase(s: Treasury): BaseTreasury { return { - withdrawn: BigInt(s.withdrawn), + withdrawn: s.withdrawn.basisPoints, }; } export function treasuryFromBase(s: BaseTreasury): Treasury { return { - withdrawn: Number(s.withdrawn), + withdrawn: lamports(s.withdrawn), }; } diff --git a/clients/js/test/plugins/collection/treasury.test.ts b/clients/js/test/plugins/collection/treasury.test.ts index a1f05957..f553dcc8 100644 --- a/clients/js/test/plugins/collection/treasury.test.ts +++ b/clients/js/test/plugins/collection/treasury.test.ts @@ -41,7 +41,7 @@ test('it can add treasury to collection', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: lamports(0), }, }); }); @@ -69,7 +69,7 @@ test('it can create collection with treasury', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: lamports(0), }, }); }); @@ -84,7 +84,7 @@ test('it cannot create treasury with nonzero withdrawn', async (t) => { plugins: [ pluginAuthorityPairV2({ type: 'Treasury', - withdrawn: 10, + withdrawn: lamports(10), }), ], }).sendAndConfirm(umi); @@ -176,7 +176,7 @@ test('it can withdraw SOL from treasury', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: lamports(0), }, }); @@ -193,7 +193,7 @@ test('it can withdraw SOL from treasury', async (t) => { collection: collection.publicKey, plugin: { type: 'Treasury', - withdrawn: 1_000_000, + withdrawn: lamports(1_000_000), }, }).sendAndConfirm(umi); @@ -218,7 +218,7 @@ test('it can withdraw SOL from treasury', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 1_000_000, + withdrawn: lamports(1_000_000), }, }); }); @@ -247,7 +247,7 @@ test('it cannot withdraw SOL from treasury if not the authority', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: lamports(0), }, }); @@ -258,7 +258,7 @@ test('it cannot withdraw SOL from treasury if not the authority', async (t) => { authority, plugin: { type: 'Treasury', - withdrawn: 1_000_000, + withdrawn: lamports(1_000_000), }, }).sendAndConfirm(umi); @@ -288,7 +288,7 @@ test('it cannot withdraw more than excess rent from treasury', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: lamports(0), }, }); @@ -298,7 +298,7 @@ test('it cannot withdraw more than excess rent from treasury', async (t) => { collection: collection.publicKey, plugin: { type: 'Treasury', - withdrawn: 1_000_001, + withdrawn: lamports(1_000_001), }, }).sendAndConfirm(umi); @@ -326,7 +326,7 @@ test('it cannot withdraw entire balance from treasury', async (t) => { collection: collection.publicKey, plugin: { type: 'Treasury', - withdrawn: Number(collection.header.lamports.basisPoints), + withdrawn: collection.header.lamports, }, }).sendAndConfirm(umi); @@ -342,7 +342,7 @@ test('it cannot withdraw entire balance from treasury', async (t) => { authority: { type: 'UpdateAuthority', }, - withdrawn: 0, + withdrawn: lamports(0), }, }); }); 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/programs/mpl-core/src/plugins/treasury.rs b/programs/mpl-core/src/plugins/treasury.rs index ced1d9cf..20d39285 100644 --- a/programs/mpl-core/src/plugins/treasury.rs +++ b/programs/mpl-core/src/plugins/treasury.rs @@ -1,7 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use solana_program::{ - program::invoke, program_error::ProgramError, rent::Rent, system_instruction, sysvar::Sysvar, -}; +use solana_program::{program_error::ProgramError, rent::Rent, sysvar::Sysvar}; use crate::error::MplCoreError; From c4fb33e726934d0ce1cb81c5b6265286f4571fe1 Mon Sep 17 00:00:00 2001 From: Blockiosaurus Date: Sat, 17 Aug 2024 20:38:22 -0400 Subject: [PATCH 6/6] Switching back to u64 --- clients/js/src/generated/types/baseTreasury.ts | 4 ++-- clients/rust/src/generated/types/treasury.rs | 2 +- idls/mpl_core.json | 2 +- programs/mpl-core/src/plugins/treasury.rs | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/clients/js/src/generated/types/baseTreasury.ts b/clients/js/src/generated/types/baseTreasury.ts index 32603dc3..a33dd03f 100644 --- a/clients/js/src/generated/types/baseTreasury.ts +++ b/clients/js/src/generated/types/baseTreasury.ts @@ -6,7 +6,7 @@ * @see https://github.com/metaplex-foundation/kinobi */ -import { Serializer, i64, struct } from '@metaplex-foundation/umi/serializers'; +import { Serializer, struct, u64 } from '@metaplex-foundation/umi/serializers'; export type BaseTreasury = { withdrawn: bigint }; @@ -16,7 +16,7 @@ export function getBaseTreasurySerializer(): Serializer< BaseTreasuryArgs, BaseTreasury > { - return struct([['withdrawn', i64()]], { + return struct([['withdrawn', u64()]], { description: 'BaseTreasury', }) as Serializer; } diff --git a/clients/rust/src/generated/types/treasury.rs b/clients/rust/src/generated/types/treasury.rs index db2fbce7..46ac8037 100644 --- a/clients/rust/src/generated/types/treasury.rs +++ b/clients/rust/src/generated/types/treasury.rs @@ -15,5 +15,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg_attr(feature = "anchor", derive(AnchorSerialize, AnchorDeserialize))] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Treasury { - pub withdrawn: i64, + pub withdrawn: u64, } diff --git a/idls/mpl_core.json b/idls/mpl_core.json index 9fe74884..c8b66ed3 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -3059,7 +3059,7 @@ "fields": [ { "name": "withdrawn", - "type": "i64" + "type": "u64" } ] } diff --git a/programs/mpl-core/src/plugins/treasury.rs b/programs/mpl-core/src/plugins/treasury.rs index 20d39285..f3a75fc4 100644 --- a/programs/mpl-core/src/plugins/treasury.rs +++ b/programs/mpl-core/src/plugins/treasury.rs @@ -12,7 +12,7 @@ use super::{ #[derive(Clone, BorshSerialize, BorshDeserialize, Default, Debug, PartialEq, Eq)] pub struct Treasury { /// How much SOL has been withdrawn from the treasury, in lamports - pub withdrawn: i64, + pub withdrawn: u64, } impl PluginValidation for Treasury { @@ -77,9 +77,7 @@ impl PluginValidation for Treasury { let diff: u64 = treasury .withdrawn .checked_sub(self.withdrawn) - .ok_or(MplCoreError::NumericalOverflow)? - .try_into() - .map_err(|_| MplCoreError::NumericalOverflow)?; + .ok_or(MplCoreError::NumericalOverflow)?; if diff > excess_rent { return Err(MplCoreError::CannotOverdraw.into());