diff --git a/README.md b/README.md index cde5775a..8539f246 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ advantage of some of the latest features of a specific token program, this might * **Bidirectional Wrapping:** Convert tokens between SPL Token and SPL Token 2022 standards in either direction, including conversions between different SPL Token 2022 mints. -* **SPL Token 2022 Extension Support:** Preserve or add SPL Token 2022 extensions (like transfer fees, confidential - transfers, etc.) during the wrapping process. Note: this requires forking and updating the `CreateMint` instruction. +* **Extensible Mint Creation:** The `CreateMint` instruction is designed to be extensible through the `MintCustomizer` + trait. By forking the program and implementing this trait, developers can add custom logic to: + * Include any SPL Token 2022 extensions on the new wrapped mint. + * Modify default properties like the `freeze_authority` and `decimals`. * **Transfer Hook Compatibility:** Integrates with tokens that implement the SPL Transfer Hook interface, enabling custom logic on token transfers. * **Multisignature Support:** Compatible with multisig signers for both wrapping and unwrapping operations. diff --git a/program/src/lib.rs b/program/src/lib.rs index 0f4ddba8..5ad76bdb 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -5,6 +5,7 @@ mod entrypoint; pub mod error; pub mod instruction; +pub mod mint_customizer; pub mod processor; pub mod state; diff --git a/program/src/mint_customizer/interface.rs b/program/src/mint_customizer/interface.rs new file mode 100644 index 00000000..916c451b --- /dev/null +++ b/program/src/mint_customizer/interface.rs @@ -0,0 +1,33 @@ +use { + solana_account_info::AccountInfo, + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, +}; + +/// The interface for customizing attributes of the new wrapped mint. +pub trait MintCustomizer { + /// Calculates the total space required for a new spl-token-2022 mint + /// account, including any custom extensions + fn get_token_2022_mint_space( + &self, + unwrapped_mint_account: &AccountInfo, + all_accounts: &[AccountInfo], + ) -> Result; + + /// Customizes initialization for the extensions for the wrapped mint + /// (only relevant if creating spl-token-2022 mint) + fn initialize_extensions( + &self, + wrapped_mint_account: &AccountInfo, + unwrapped_mint_account: &AccountInfo, + wrapped_token_program_account: &AccountInfo, + all_accounts: &[AccountInfo], + ) -> ProgramResult; + + /// Customize the freeze authority and decimals for the wrapped mint + fn get_freeze_auth_and_decimals( + &self, + unwrapped_mint_account: &AccountInfo, + all_accounts: &[AccountInfo], + ) -> Result<(Option, u8), ProgramError>; +} diff --git a/program/src/mint_customizer/mod.rs b/program/src/mint_customizer/mod.rs new file mode 100644 index 00000000..a855179c --- /dev/null +++ b/program/src/mint_customizer/mod.rs @@ -0,0 +1,6 @@ +//! Mint `customizer` interface and implementations + +/// `MintCustomizer` trait definition +pub mod interface; +/// No extensions version of the mint +pub mod no_extensions; diff --git a/program/src/mint_customizer/no_extensions.rs b/program/src/mint_customizer/no_extensions.rs new file mode 100644 index 00000000..a97d8639 --- /dev/null +++ b/program/src/mint_customizer/no_extensions.rs @@ -0,0 +1,48 @@ +use { + crate::mint_customizer::interface::MintCustomizer, + solana_account_info::AccountInfo, + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, + spl_token_2022::{ + extension::{ExtensionType, PodStateWithExtensions}, + pod::PodMint, + state::Mint, + }, +}; + +/// This implementation does not add any extensions. +pub struct NoExtensionCustomizer; + +impl MintCustomizer for NoExtensionCustomizer { + fn get_token_2022_mint_space( + &self, + _unwrapped_mint_account: &AccountInfo, + _all_accounts: &[AccountInfo], + ) -> Result { + let extensions = vec![]; + ExtensionType::try_calculate_account_len::(&extensions) + } + + fn initialize_extensions( + &self, + _wrapped_mint_account: &AccountInfo, + _unwrapped_mint_account: &AccountInfo, + _wrapped_token_program_account: &AccountInfo, + _all_accounts: &[AccountInfo], + ) -> ProgramResult { + Ok(()) + } + + fn get_freeze_auth_and_decimals( + &self, + unwrapped_mint_account: &AccountInfo, + _all_accounts: &[AccountInfo], + ) -> Result<(Option, u8), ProgramError> { + // Copy fields over from original mint + let unwrapped_mint_data = unwrapped_mint_account.try_borrow_data()?; + let pod_mint = PodStateWithExtensions::::unpack(&unwrapped_mint_data)?.base; + let freeze_authority = pod_mint.freeze_authority.ok_or(()).ok(); + let decimals = pod_mint.decimals; + Ok((freeze_authority, decimals)) + } +} diff --git a/program/src/processor.rs b/program/src/processor.rs index 8dfd9acf..9d493ff6 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -2,11 +2,14 @@ use { crate::{ - error::TokenWrapError, get_wrapped_mint_address, get_wrapped_mint_address_with_seed, - get_wrapped_mint_authority, get_wrapped_mint_authority_signer_seeds, - get_wrapped_mint_authority_with_seed, get_wrapped_mint_backpointer_address_signer_seeds, + error::TokenWrapError, + get_wrapped_mint_address, get_wrapped_mint_address_with_seed, get_wrapped_mint_authority, + get_wrapped_mint_authority_signer_seeds, get_wrapped_mint_authority_with_seed, + get_wrapped_mint_backpointer_address_signer_seeds, get_wrapped_mint_backpointer_address_with_seed, get_wrapped_mint_signer_seeds, - instruction::TokenWrapInstruction, state::Backpointer, + instruction::TokenWrapInstruction, + mint_customizer::{interface::MintCustomizer, no_extensions::NoExtensionCustomizer}, + state::Backpointer, }, solana_account_info::{next_account_info, AccountInfo}, solana_cpi::{invoke, invoke_signed}, @@ -33,10 +36,11 @@ use { }; /// Processes [`CreateMint`](enum.TokenWrapInstruction.html) instruction. -pub fn process_create_mint( +pub fn process_create_mint( program_id: &Pubkey, accounts: &[AccountInfo], idempotent: bool, + mint_customizer: M, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -95,7 +99,12 @@ pub fn process_create_mint( wrapped_token_program_account.key, &bump_seed, ); - let space = spl_token_2022::state::Mint::get_packed_len(); + + let space = if *wrapped_token_program_account.key == spl_token_2022::id() { + mint_customizer.get_token_2022_mint_space(unwrapped_mint_account, accounts)? + } else { + spl_token::state::Mint::get_packed_len() + }; let rent = Rent::get()?; let mint_rent_required = rent.minimum_balance(space); @@ -120,18 +129,21 @@ pub fn process_create_mint( &[&signer_seeds], )?; - // New wrapped mint matches decimals & freeze authority of unwrapped mint - let unwrapped_mint_data = unwrapped_mint_account.try_borrow_data()?; - let unpacked_unwrapped_mint = - PodStateWithExtensions::::unpack(&unwrapped_mint_data)?.base; - let decimals = unpacked_unwrapped_mint.decimals; - let freeze_authority = unpacked_unwrapped_mint - .freeze_authority - .ok_or(ProgramError::InvalidArgument) - .ok(); - let wrapped_mint_authority = get_wrapped_mint_authority(wrapped_mint_account.key); + // If wrapping into a token-2022 initialize extensions + if *wrapped_token_program_account.key == spl_token_2022::id() { + mint_customizer.initialize_extensions( + wrapped_mint_account, + unwrapped_mint_account, + wrapped_token_program_account, + accounts, + )?; + } + + let (freeze_authority, decimals) = + mint_customizer.get_freeze_auth_and_decimals(unwrapped_mint_account, accounts)?; + invoke( &initialize_mint2( wrapped_token_program_account.key, @@ -490,8 +502,10 @@ pub fn process_instruction( ) -> ProgramResult { match TokenWrapInstruction::unpack(input)? { TokenWrapInstruction::CreateMint { idempotent } => { + // === DEVELOPER CUSTOMIZATION POINT === + // To use custom mint creation logic, update the mint customizer argument msg!("Instruction: CreateMint"); - process_create_mint(program_id, accounts, idempotent) + process_create_mint(program_id, accounts, idempotent, NoExtensionCustomizer) } TokenWrapInstruction::Wrap { amount } => { msg!("Instruction: Wrap"); diff --git a/program/tests/test_create_mint.rs b/program/tests/test_create_mint.rs index 4bab8493..cf8da700 100644 --- a/program/tests/test_create_mint.rs +++ b/program/tests/test_create_mint.rs @@ -8,14 +8,13 @@ use { mollusk_svm::result::Check, solana_account::Account, solana_program_error::ProgramError, - solana_program_option::COption, solana_program_pack::Pack, solana_pubkey::Pubkey, solana_rent::Rent, spl_pod::primitives::{PodBool, PodU64}, spl_token_2022::{ - extension::PodStateWithExtensions, - pod::{PodCOption, PodMint}, + extension::{BaseStateWithExtensions, PodStateWithExtensions}, + pod::PodMint, state::Mint, }, spl_token_wrap::{ @@ -202,7 +201,6 @@ fn test_successful_spl_token_to_spl_token_2022() { assert_eq!(result.wrapped_mint.account.owner, spl_token_2022::id()); let wrapped_mint_data = Mint::unpack(&result.wrapped_mint.account.data).unwrap(); - assert_eq!(wrapped_mint_data.decimals, DEFAULT_MINT_DECIMALS); let expected_mint_authority = get_wrapped_mint_authority(&result.wrapped_mint.key); assert_eq!( wrapped_mint_data.mint_authority.unwrap(), @@ -210,13 +208,6 @@ fn test_successful_spl_token_to_spl_token_2022() { ); assert_eq!(wrapped_mint_data.supply, 0); assert!(wrapped_mint_data.is_initialized); - assert_eq!( - wrapped_mint_data.freeze_authority, - COption::Some(freeze_authority) - ); - - // Assert state of resulting backpointer account - assert_eq!( result.wrapped_backpointer.account.owner, spl_token_wrap::id() @@ -251,7 +242,6 @@ fn test_successful_spl_token_2022_to_spl_token() { .unwrap() .base; - assert_eq!(wrapped_mint_data.decimals, DEFAULT_MINT_DECIMALS); let expected_mint_authority = get_wrapped_mint_authority(&wrapped_mint_address); assert_eq!( wrapped_mint_data @@ -262,10 +252,6 @@ fn test_successful_spl_token_2022_to_spl_token() { ); assert_eq!(wrapped_mint_data.supply, PodU64::from(0)); assert_eq!(wrapped_mint_data.is_initialized, PodBool::from_bool(true)); - assert_eq!( - wrapped_mint_data.freeze_authority, - PodCOption::some(freeze_authority) - ); // Assert state of resulting backpointer account @@ -302,7 +288,6 @@ fn test_create_mint_from_extended_mint(extension: MintExtension) { .unwrap() .base; - assert_eq!(wrapped_mint_data.decimals, DEFAULT_MINT_DECIMALS); let expected_mint_authority = get_wrapped_mint_authority(&result.wrapped_mint.key); assert_eq!( wrapped_mint_data @@ -313,7 +298,6 @@ fn test_create_mint_from_extended_mint(extension: MintExtension) { ); assert_eq!(wrapped_mint_data.supply, PodU64::from(0)); assert_eq!(wrapped_mint_data.is_initialized, PodBool::from_bool(true)); - assert_eq!(wrapped_mint_data.freeze_authority, PodCOption::none()); assert_eq!( result.wrapped_backpointer.account.owner, @@ -323,3 +307,46 @@ fn test_create_mint_from_extended_mint(extension: MintExtension) { bytemuck::from_bytes::(&result.wrapped_backpointer.account.data[..]); assert_eq!(backpointer.unwrapped_mint, unwrapped_mint.key); } + +// ============= Mint Customizer Tests ============= +// If you are forking this program and adding your own mint customizer, +// you should modify/add tests in this section to validate your custom +// implementation. + +#[test_case(TokenProgram::SplToken, TokenProgram::SplToken)] +#[test_case(TokenProgram::SplToken, TokenProgram::SplToken2022)] +#[test_case(TokenProgram::SplToken2022, TokenProgram::SplToken)] +#[test_case(TokenProgram::SplToken2022, TokenProgram::SplToken2022)] +fn test_mint_customizer_copies_decimals_and_freeze_authority(from: TokenProgram, to: TokenProgram) { + let freeze_authority = Pubkey::new_unique(); + let result = CreateMintBuilder::default() + .unwrapped_token_program(from) + .wrapped_token_program(to) + .freeze_authority(freeze_authority) + .execute(); + + let wrapped_mint_data = + PodStateWithExtensions::::unpack(&result.wrapped_mint.account.data).unwrap(); + + assert_eq!( + wrapped_mint_data.base.freeze_authority.ok_or(()).unwrap(), + freeze_authority + ); + assert_eq!(wrapped_mint_data.base.decimals, DEFAULT_MINT_DECIMALS); +} + +#[test] +fn test_customizer_does_not_apply_extensions() { + let result = CreateMintBuilder::default() + .wrapped_token_program(TokenProgram::SplToken2022) + .execute(); + + let extensions_on_mint = + PodStateWithExtensions::::unpack(&result.wrapped_mint.account.data) + .unwrap() + .get_extension_types() + .unwrap() + .len(); + + assert_eq!(0, extensions_on_mint); +}