diff --git a/clients/js/src/generated/errors/mplCore.ts b/clients/js/src/generated/errors/mplCore.ts index 4254682a..cb0f0b4a 100644 --- a/clients/js/src/generated/errors/mplCore.ts +++ b/clients/js/src/generated/errors/mplCore.ts @@ -752,6 +752,19 @@ export class CannotOverdrawError extends ProgramError { codeToErrorMap.set(0x33, CannotOverdrawError); nameToErrorMap.set('CannotOverdraw', CannotOverdrawError); +/** PluginRequiresCollection: Plugin requires a collection */ +export class PluginRequiresCollectionError extends ProgramError { + override readonly name: string = 'PluginRequiresCollection'; + + readonly code: number = 0x34; // 52 + + constructor(program: Program, cause?: Error) { + super('Plugin requires a collection', program, cause); + } +} +codeToErrorMap.set(0x34, PluginRequiresCollectionError); +nameToErrorMap.set('PluginRequiresCollection', PluginRequiresCollectionError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/src/generated/types/baseSolTransferFee.ts b/clients/js/src/generated/types/baseSolTransferFee.ts new file mode 100644 index 00000000..2d8fd124 --- /dev/null +++ b/clients/js/src/generated/types/baseSolTransferFee.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 BaseSolTransferFee = { feeAmount: bigint }; + +export type BaseSolTransferFeeArgs = { feeAmount: number | bigint }; + +export function getBaseSolTransferFeeSerializer(): Serializer< + BaseSolTransferFeeArgs, + BaseSolTransferFee +> { + return struct([['feeAmount', u64()]], { + description: 'BaseSolTransferFee', + }) as Serializer; +} diff --git a/clients/js/src/generated/types/index.ts b/clients/js/src/generated/types/index.ts index 398c4b1a..905264cf 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 './baseSolTransferFee'; export * from './baseTreasury'; export * from './baseUpdateAuthority'; export * from './baseValidationResultsOffset'; diff --git a/clients/js/src/generated/types/plugin.ts b/clients/js/src/generated/types/plugin.ts index ad554bf1..9066e9d3 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, + BaseSolTransferFee, + BaseSolTransferFeeArgs, BaseTreasury, BaseTreasuryArgs, BurnDelegate, @@ -52,6 +54,7 @@ import { getAutographSerializer, getBaseMasterEditionSerializer, getBaseRoyaltiesSerializer, + getBaseSolTransferFeeSerializer, getBaseTreasurySerializer, getBurnDelegateSerializer, getEditionSerializer, @@ -81,7 +84,8 @@ export type Plugin = | { __kind: 'ImmutableMetadata'; fields: [ImmutableMetadata] } | { __kind: 'VerifiedCreators'; fields: [VerifiedCreators] } | { __kind: 'Autograph'; fields: [Autograph] } - | { __kind: 'Treasury'; fields: [BaseTreasury] }; + | { __kind: 'Treasury'; fields: [BaseTreasury] } + | { __kind: 'SolTransferFee'; fields: [BaseSolTransferFee] }; export type PluginArgs = | { __kind: 'Royalties'; fields: [BaseRoyaltiesArgs] } @@ -102,7 +106,8 @@ export type PluginArgs = | { __kind: 'ImmutableMetadata'; fields: [ImmutableMetadataArgs] } | { __kind: 'VerifiedCreators'; fields: [VerifiedCreatorsArgs] } | { __kind: 'Autograph'; fields: [AutographArgs] } - | { __kind: 'Treasury'; fields: [BaseTreasuryArgs] }; + | { __kind: 'Treasury'; fields: [BaseTreasuryArgs] } + | { __kind: 'SolTransferFee'; fields: [BaseSolTransferFeeArgs] }; export function getPluginSerializer(): Serializer { return dataEnum( @@ -203,6 +208,12 @@ export function getPluginSerializer(): Serializer { ['fields', tuple([getBaseTreasurySerializer()])], ]), ], + [ + 'SolTransferFee', + struct>([ + ['fields', tuple([getBaseSolTransferFeeSerializer()])], + ]), + ], ], { description: 'Plugin' } ) as Serializer; @@ -276,6 +287,10 @@ export function plugin( kind: 'Treasury', data: GetDataEnumKindContent['fields'] ): GetDataEnumKind; +export function plugin( + kind: 'SolTransferFee', + 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 e43190ff..7da48854 100644 --- a/clients/js/src/generated/types/pluginType.ts +++ b/clients/js/src/generated/types/pluginType.ts @@ -25,6 +25,7 @@ export enum PluginType { VerifiedCreators, Autograph, Treasury, + SolTransferFee, } export type PluginTypeArgs = PluginType; diff --git a/clients/js/src/plugins/lib.ts b/clients/js/src/plugins/lib.ts index 961f3e29..16ecfd89 100644 --- a/clients/js/src/plugins/lib.ts +++ b/clients/js/src/plugins/lib.ts @@ -28,6 +28,7 @@ import { import { royaltiesFromBase, royaltiesToBase } from './royalties'; import { masterEditionFromBase, masterEditionToBase } from './masterEdition'; import { treasuryFromBase, treasuryToBase } from './treasury'; +import { solTransferFeeFromBase, solTransferFeeToBase } from './solTransferFee'; export function formPluginHeaderV1( pluginRegistryOffset: bigint @@ -111,6 +112,13 @@ export function createPluginV2(args: AssetAllPluginArgsV2): BasePlugin { }; } + if (type === 'SolTransferFee') { + return { + __kind: type, + fields: [solTransferFeeToBase(args)], + }; + } + return { __kind: type, fields: [(args as any) || {}], @@ -184,6 +192,16 @@ export function mapPlugin({ }; } + if (plug.__kind === 'SolTransferFee') { + return { + [pluginKey]: { + authority, + offset, + ...solTransferFeeFromBase(plug.fields[0]), + }, + }; + } + return { [pluginKey]: { authority, diff --git a/clients/js/src/plugins/solTransferFee.ts b/clients/js/src/plugins/solTransferFee.ts new file mode 100644 index 00000000..22a3ce7a --- /dev/null +++ b/clients/js/src/plugins/solTransferFee.ts @@ -0,0 +1,20 @@ +import { lamports, SolAmount } from '@metaplex-foundation/umi'; +import { BaseSolTransferFee } from '../generated'; + +export type SolTransferFee = { + feeAmount: SolAmount; +}; + +export type SolTransferFeeArgs = SolTransferFee; + +export function solTransferFeeToBase(s: SolTransferFee): BaseSolTransferFee { + return { + feeAmount: s.feeAmount.basisPoints, + }; +} + +export function solTransferFeeFromBase(s: BaseSolTransferFee): SolTransferFee { + return { + feeAmount: lamports(s.feeAmount), + }; +} diff --git a/clients/js/src/plugins/types.ts b/clients/js/src/plugins/types.ts index 6c647923..96fee843 100644 --- a/clients/js/src/plugins/types.ts +++ b/clients/js/src/plugins/types.ts @@ -26,11 +26,13 @@ import { Autograph, VerifiedCreators, BaseTreasuryArgs, + BaseSolTransferFeeArgs, } from '../generated'; import { RoyaltiesArgs, RoyaltiesPlugin } from './royalties'; import { PluginAuthority } from './pluginAuthority'; import { MasterEdition, MasterEditionArgs } from './masterEdition'; import { Treasury, TreasuryArgs } from './treasury'; +import { SolTransferFee, SolTransferFeeArgs } from './solTransferFee'; // for backwards compatibility export { pluginAuthority, updateAuthority, ruleSet }; @@ -94,6 +96,10 @@ export type CreatePluginArgs = | { type: 'Treasury'; data: BaseTreasuryArgs; + } + | { + type: 'SolTransferFee'; + data: BaseSolTransferFeeArgs; }; export type AuthorityArgsV2 = { @@ -152,7 +158,10 @@ export type AuthorityManagedPluginArgsV2 = } & VerifiedCreatorsArgs) | ({ type: 'Treasury'; - } & TreasuryArgs); + } & TreasuryArgs) + | ({ + type: 'SolTransferFee'; + } & SolTransferFeeArgs); export type AssetAddablePluginArgsV2 = | OwnerManagedPluginArgsV2 @@ -191,6 +200,7 @@ export type ImmutableMetadataPlugin = BasePlugin & ImmutableMetadata; export type VerifiedCreatorsPlugin = BasePlugin & VerifiedCreators; export type AutographPlugin = BasePlugin & Autograph; export type TreasuryPlugin = BasePlugin & Treasury; +export type SolTransferFeePlugin = BasePlugin & SolTransferFee; export type CommonPluginsList = { attributes?: AttributesPlugin; @@ -210,6 +220,7 @@ export type AssetPluginsList = { burnDelegate?: BurnDelegatePlugin; transferDelegate?: TransferDelegatePlugin; edition?: EditionPlugin; + solTransferFee?: SolTransferFeePlugin; } & CommonPluginsList; export type CollectionPluginsList = { diff --git a/clients/js/test/plugins/asset/solTransferFee.test.ts b/clients/js/test/plugins/asset/solTransferFee.test.ts new file mode 100644 index 00000000..a097c0ad --- /dev/null +++ b/clients/js/test/plugins/asset/solTransferFee.test.ts @@ -0,0 +1,183 @@ +import test from 'ava'; + +import { generateSigner, lamports } from '@metaplex-foundation/umi'; +import { createAssetWithCollection } from '../../_setupSdk'; +import { + createCollectionV2, + pluginAuthorityPairV2, + addPlugin, + createV2, + addCollectionPlugin, + transfer, +} from '../../../src'; +import { + DEFAULT_ASSET, + DEFAULT_COLLECTION, + assertAsset, + createCollection, + createUmi, +} from '../../_setupRaw'; + +test('it can add sol transfer fee to asset', async (t) => { + const umi = await createUmi(); + const { asset, collection } = await createAssetWithCollection(umi, {}); + + await addPlugin(umi, { + asset: asset.publicKey, + collection: collection.publicKey, + plugin: { + type: 'SolTransferFee', + feeAmount: lamports(1_000_000), + }, + }).sendAndConfirm(umi); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + solTransferFee: { + authority: { + type: 'UpdateAuthority', + }, + feeAmount: lamports(1_000_000), + }, + }); +}); + +test('it can create asset with sol transfer fee', async (t) => { + const umi = await createUmi(); + + const { asset, collection } = await createAssetWithCollection(umi, { + plugins: [ + { + type: 'SolTransferFee', + feeAmount: lamports(1_000_000), + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + solTransferFee: { + authority: { + type: 'UpdateAuthority', + }, + feeAmount: lamports(1_000_000), + }, + }); +}); + +test('it cannot create sol transfer fee without a collection', async (t) => { + const umi = await createUmi(); + const asset = generateSigner(umi); + + const result = createV2(umi, { + asset, + plugins: [ + pluginAuthorityPairV2({ + type: 'SolTransferFee', + feeAmount: lamports(1_000_000), + }), + ], + name: DEFAULT_ASSET.name, + uri: DEFAULT_ASSET.uri, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'PluginRequiresCollection', + }); +}); + +test('it cannot add sol transfer fee to a collection', async (t) => { + const umi = await createUmi(); + const collection = await createCollection(umi); + + const result = addCollectionPlugin(umi, { + collection: collection.publicKey, + plugin: { + type: 'SolTransferFee', + feeAmount: lamports(1_000_000), + }, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'PluginNotAllowedOnCollection', + }); +}); + +test('it cannot create collection with sol transfer fee', async (t) => { + const umi = await createUmi(); + const collection = generateSigner(umi); + + const result = createCollectionV2(umi, { + ...DEFAULT_COLLECTION, + collection, + plugins: [ + pluginAuthorityPairV2({ + type: 'SolTransferFee', + feeAmount: lamports(1_000_000), + }), + ], + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { + name: 'PluginNotAllowedOnCollection', + }); +}); + +test('it pays SOL on transfer', async (t) => { + const umi = await createUmi(); + const recipient = generateSigner(umi); + + const { asset, collection } = await createAssetWithCollection(umi, { + plugins: [ + { + type: 'SolTransferFee', + feeAmount: lamports(1_000_000), + }, + ], + }); + + await assertAsset(t, umi, { + ...DEFAULT_ASSET, + asset: asset.publicKey, + owner: umi.identity.publicKey, + updateAuthority: { type: 'Collection', address: collection.publicKey }, + solTransferFee: { + authority: { + type: 'UpdateAuthority', + }, + feeAmount: lamports(1_000_000), + }, + }); + + const identityBeforeBalance = await umi.rpc.getBalance( + umi.identity.publicKey + ); + const collectionBeforeBalance = await umi.rpc.getBalance( + collection.publicKey + ); + + await transfer(umi, { + asset, + collection, + newOwner: recipient.publicKey, + }).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 - // Transfer fee + 5_000n; // Transaction fee + t.is(identityExpected, identityAfterBalance.basisPoints); + t.is( + collectionBeforeBalance.basisPoints + 1_000_000n, + collectionAfterBalance.basisPoints + ); +}); diff --git a/clients/rust/src/generated/errors/mpl_core.rs b/clients/rust/src/generated/errors/mpl_core.rs index ecd28390..5eee7a07 100644 --- a/clients/rust/src/generated/errors/mpl_core.rs +++ b/clients/rust/src/generated/errors/mpl_core.rs @@ -166,6 +166,9 @@ pub enum MplCoreError { /// 51 (0x33) - Cannot withdraw more than excess rent from treasury #[error("Cannot withdraw more than excess rent from treasury")] CannotOverdraw, + /// 52 (0x34) - Plugin requires a collection + #[error("Plugin requires a collection")] + PluginRequiresCollection, } 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 7cf4e2bd..e27fff61 100644 --- a/clients/rust/src/generated/types/mod.rs +++ b/clients/rust/src/generated/types/mod.rs @@ -63,6 +63,7 @@ pub(crate) mod r#registry_record; pub(crate) mod r#royalties; pub(crate) mod r#rule_set; pub(crate) mod r#seed; +pub(crate) mod r#sol_transfer_fee; pub(crate) mod r#transfer_delegate; pub(crate) mod r#treasury; pub(crate) mod r#update_authority; @@ -130,6 +131,7 @@ pub use self::r#registry_record::*; pub use self::r#royalties::*; pub use self::r#rule_set::*; pub use self::r#seed::*; +pub use self::r#sol_transfer_fee::*; pub use self::r#transfer_delegate::*; pub use self::r#treasury::*; pub use self::r#update_authority::*; diff --git a/clients/rust/src/generated/types/plugin.rs b/clients/rust/src/generated/types/plugin.rs index 9f9a4057..df11f210 100644 --- a/clients/rust/src/generated/types/plugin.rs +++ b/clients/rust/src/generated/types/plugin.rs @@ -17,6 +17,7 @@ use crate::generated::types::PermanentBurnDelegate; use crate::generated::types::PermanentFreezeDelegate; use crate::generated::types::PermanentTransferDelegate; use crate::generated::types::Royalties; +use crate::generated::types::SolTransferFee; use crate::generated::types::TransferDelegate; use crate::generated::types::Treasury; use crate::generated::types::UpdateDelegate; @@ -47,4 +48,5 @@ pub enum Plugin { VerifiedCreators(VerifiedCreators), Autograph(Autograph), Treasury(Treasury), + SolTransferFee(SolTransferFee), } diff --git a/clients/rust/src/generated/types/plugin_type.rs b/clients/rust/src/generated/types/plugin_type.rs index 0346ca23..2d4d6fd5 100644 --- a/clients/rust/src/generated/types/plugin_type.rs +++ b/clients/rust/src/generated/types/plugin_type.rs @@ -32,4 +32,5 @@ pub enum PluginType { VerifiedCreators, Autograph, Treasury, + SolTransferFee, } diff --git a/clients/rust/src/generated/types/sol_transfer_fee.rs b/clients/rust/src/generated/types/sol_transfer_fee.rs new file mode 100644 index 00000000..0e502fdc --- /dev/null +++ b/clients/rust/src/generated/types/sol_transfer_fee.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 SolTransferFee { + pub fee_amount: u64, +} diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index eb1df99c..2f157247 100755 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -283,6 +283,9 @@ kinobi.update( treasury: { name: "baseTreasury" }, + solTransferFee: { + name: "baseSolTransferFee" + }, }) ) diff --git a/idls/mpl_core.json b/idls/mpl_core.json index c8b66ed3..f0cccee2 100644 --- a/idls/mpl_core.json +++ b/idls/mpl_core.json @@ -3045,6 +3045,18 @@ ] } }, + { + "name": "SolTransferFee", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feeAmount", + "type": "u64" + } + ] + } + }, { "name": "TransferDelegate", "type": { @@ -3917,6 +3929,14 @@ "defined": "Treasury" } ] + }, + { + "name": "SolTransferFee", + "fields": [ + { + "defined": "SolTransferFee" + } + ] } ] } @@ -3973,6 +3993,9 @@ }, { "name": "Treasury" + }, + { + "name": "SolTransferFee" } ] } @@ -4895,6 +4918,11 @@ "code": 51, "name": "CannotOverdraw", "msg": "Cannot withdraw more than excess rent from treasury" + }, + { + "code": 52, + "name": "PluginRequiresCollection", + "msg": "Plugin requires a collection" } ], "metadata": { diff --git a/programs/mpl-core/src/error.rs b/programs/mpl-core/src/error.rs index 03e6890d..f879434a 100644 --- a/programs/mpl-core/src/error.rs +++ b/programs/mpl-core/src/error.rs @@ -216,6 +216,10 @@ pub enum MplCoreError { /// 51 - Cannot withdraw more than excess rent from treasury #[error("Cannot withdraw more than excess rent from treasury")] CannotOverdraw, + + /// 52 - Plugin requires a collection + #[error("Plugin requires a collection")] + PluginRequiresCollection, } impl PrintProgramError for MplCoreError { diff --git a/programs/mpl-core/src/plugins/lifecycle.rs b/programs/mpl-core/src/plugins/lifecycle.rs index 12bcb186..bff76a20 100644 --- a/programs/mpl-core/src/plugins/lifecycle.rs +++ b/programs/mpl-core/src/plugins/lifecycle.rs @@ -137,6 +137,7 @@ impl PluginType { PluginType::VerifiedCreators => CheckResult::CanReject, PluginType::MasterEdition => CheckResult::CanReject, PluginType::Treasury => CheckResult::CanReject, + PluginType::SolTransferFee => CheckResult::CanReject, _ => CheckResult::None, } } @@ -170,6 +171,7 @@ impl PluginType { PluginType::TransferDelegate => CheckResult::CanApprove, PluginType::PermanentFreezeDelegate => CheckResult::CanReject, PluginType::PermanentTransferDelegate => CheckResult::CanApprove, + PluginType::SolTransferFee => CheckResult::CanReject, _ => CheckResult::None, } } @@ -248,6 +250,7 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_add_plugin(ctx), Plugin::Treasury(treasury) => treasury.validate_add_plugin(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_add_plugin(ctx), } } @@ -290,6 +293,9 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_remove_plugin(ctx), Plugin::Treasury(treasury) => treasury.validate_remove_plugin(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => { + sol_transfer_fee.validate_remove_plugin(ctx) + } } } @@ -339,6 +345,9 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_approve_plugin_authority(ctx), Plugin::Treasury(treasury) => treasury.validate_approve_plugin_authority(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => { + sol_transfer_fee.validate_approve_plugin_authority(ctx) + } } } @@ -400,6 +409,9 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_revoke_plugin_authority(ctx), Plugin::Treasury(treasury) => treasury.validate_revoke_plugin_authority(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => { + sol_transfer_fee.validate_revoke_plugin_authority(ctx) + } }?; if result == ValidationResult::Pass { @@ -437,6 +449,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), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_create(ctx), } } @@ -468,6 +481,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), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_update(ctx), } } @@ -514,6 +528,9 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_update_plugin(ctx), Plugin::Treasury(treasury) => treasury.validate_update_plugin(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => { + sol_transfer_fee.validate_update_plugin(ctx) + } }?; match (&base_result, &result) { @@ -562,6 +579,7 @@ impl Plugin { Plugin::VerifiedCreators(verified_creators) => verified_creators.validate_burn(ctx), Plugin::Autograph(autograph) => autograph.validate_burn(ctx), Plugin::Treasury(treasury) => treasury.validate_burn(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_burn(ctx), } } @@ -593,6 +611,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), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_transfer(ctx), } } @@ -624,6 +643,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), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_compress(ctx), } } @@ -659,6 +679,7 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_decompress(ctx), Plugin::Treasury(treasury) => treasury.validate_decompress(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => sol_transfer_fee.validate_decompress(ctx), } } @@ -702,6 +723,9 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_add_external_plugin_adapter(ctx), Plugin::Treasury(treasury) => treasury.validate_add_external_plugin_adapter(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => { + sol_transfer_fee.validate_add_external_plugin_adapter(ctx) + } } } @@ -747,6 +771,9 @@ impl Plugin { } Plugin::Autograph(autograph) => autograph.validate_remove_external_plugin_adapter(ctx), Plugin::Treasury(treasury) => treasury.validate_remove_external_plugin_adapter(ctx), + Plugin::SolTransferFee(sol_transfer_fee) => { + sol_transfer_fee.validate_remove_external_plugin_adapter(ctx) + } } } } diff --git a/programs/mpl-core/src/plugins/mod.rs b/programs/mpl-core/src/plugins/mod.rs index c23154e4..accc9cbf 100644 --- a/programs/mpl-core/src/plugins/mod.rs +++ b/programs/mpl-core/src/plugins/mod.rs @@ -20,6 +20,7 @@ mod permanent_transfer_delegate; mod plugin_header; mod plugin_registry; mod royalties; +mod sol_transfer_fee; mod transfer; mod treasury; mod update_delegate; @@ -48,6 +49,7 @@ pub use permanent_transfer_delegate::*; pub use plugin_header::*; pub use plugin_registry::*; pub use royalties::*; +pub use sol_transfer_fee::*; pub use transfer::*; pub use treasury::*; pub use update_delegate::*; @@ -102,6 +104,8 @@ pub enum Plugin { Autograph(Autograph), /// Treasury plugin allows for the Collection to contain a SOL treasury Treasury(Treasury), + /// SOL transfer fee plugin charges a fee for every transfer of the asset and stores it on the collection. + SolTransferFee(SolTransferFee), } impl Plugin { @@ -178,6 +182,8 @@ pub enum PluginType { Autograph, /// Treasury plugin. Treasury, + /// SOL transfer fee plugin. + SolTransferFee, } impl DataBlob for PluginType { @@ -209,6 +215,7 @@ impl From<&Plugin> for PluginType { Plugin::VerifiedCreators(_) => PluginType::VerifiedCreators, Plugin::Autograph(_) => PluginType::Autograph, Plugin::Treasury(_) => PluginType::Treasury, + Plugin::SolTransferFee(_) => PluginType::SolTransferFee, } } } @@ -233,6 +240,7 @@ impl PluginType { PluginType::VerifiedCreators => Authority::UpdateAuthority, PluginType::Autograph => Authority::Owner, PluginType::Treasury => Authority::UpdateAuthority, + PluginType::SolTransferFee => Authority::UpdateAuthority, } } } diff --git a/programs/mpl-core/src/plugins/sol_transfer_fee.rs b/programs/mpl-core/src/plugins/sol_transfer_fee.rs new file mode 100644 index 00000000..bc7e531c --- /dev/null +++ b/programs/mpl-core/src/plugins/sol_transfer_fee.rs @@ -0,0 +1,78 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{program::invoke, program_error::ProgramError, system_instruction}; + +use crate::{error::MplCoreError, plugins::abstain}; + +use super::{PluginType, PluginValidation, PluginValidationContext, ValidationResult}; + +/// The SOL transfer fee plugin charges a fee for every transfer of the asset and stores it +/// on the collection. +#[repr(C)] +#[derive(Clone, BorshSerialize, BorshDeserialize, Default, Debug, PartialEq, Eq)] +pub struct SolTransferFee { + /// The amount of SOL to charge for every transfer, in lamports + pub fee_amount: u64, +} + +impl PluginValidation for SolTransferFee { + fn validate_create( + &self, + ctx: &PluginValidationContext, + ) -> Result { + // Transfer fees are stored on the collection, so we require a collection. + if ctx.collection_info.is_none() { + return Err(MplCoreError::PluginRequiresCollection.into()); + } + + // 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 SOL transfer fee plugin on a collection. + if PluginType::from(target_plugin) == PluginType::SolTransferFee + && ctx.asset_info.is_none() + { + Err(MplCoreError::PluginNotAllowedOnCollection.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)?; + + // Transfer fees are stored on the collection, so we require a collection. + if ctx.collection_info.is_none() { + return Err(MplCoreError::PluginRequiresCollection.into()); + } + + // You can't add the SOL transfer fee plugin to a collection. + if PluginType::from(target_plugin) == PluginType::SolTransferFee && ctx.asset_info.is_none() + { + Err(MplCoreError::PluginNotAllowedOnCollection.into()) + } else { + abstain!() + } + } + + fn validate_transfer( + &self, + ctx: &PluginValidationContext, + ) -> Result { + if self.fee_amount > 0 { + let collection = ctx.collection_info.ok_or(MplCoreError::MissingCollection)?; + + invoke( + &system_instruction::transfer(ctx.payer.key, collection.key, self.fee_amount), + &[ctx.payer.clone(), collection.clone()], + )?; + } + + abstain!() + } +}