diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1868f497..d5cbf825 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -166,6 +166,7 @@ jobs: cargo-cache-key: cargo-cli cli: true purge: true + solana: true - name: Restore Program Builds uses: actions/cache/restore@v4 diff --git a/README.md b/README.md index 8539f246..5269d00d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ advantage of some of the latest features of a specific token program, this might 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`. +* **Confidential Transfers by Default:** All wrapped tokens created under the Token-2022 standard automatically include + the `ConfidentialTransferMint` extension, enabling the option for privacy-preserving transactions. This feature is + immutable and requires no additional configuration. * **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. @@ -72,6 +75,29 @@ The SPL Token Wrap program is designed to be **permissionless**. This means: Wrap program itself. However, it is important to note that if the *unwrapped* token has a freeze authority, that freeze authority is *preserved* in the wrapped token. +## Confidential Transfer extension + +The `ConfidentialTransferMint` extension is added to every Token-2022 wrapped mint and initialized with the following +config: + +* **No Authority:** The confidential transfer authority is set to `None`, making the configuration immutable. This + ensures that the privacy features cannot be disabled or altered after the wrapped mint is created. +* **No Auditor:** The wrapped mints are created without a confidential transfer auditor. This means that there is no + third party that can view the details of confidential transactions. +* **Automatic Account Approval:** New token accounts are approved for confidential transfers by default. This allows + users to make private transactions permissionlessly. + +## Customizing mint + +If the current wrapped mint config does not suit your needs, please fork! A few places you are going to want to update: + +- Add a new struct that implements `MintCustomizer` in `program/src/mint_customizer` +- Replace the current one in use within the processor: `program/src/processor.rs` +- Re-run tests (see `package.json`) and update/remove assertions to accommodate new config +- If wanting to make use of clients: + - CLI: Update mint customizer type in `clients/cli/src/create_mint.rs` + - JS: Update mint size in `clients/js/src/create-mint.ts` + ## Audits | Auditor | Date | Version | Report | diff --git a/clients/cli/src/create_mint.rs b/clients/cli/src/create_mint.rs index 3a57d884..7508522d 100644 --- a/clients/cli/src/create_mint.rs +++ b/clients/cli/src/create_mint.rs @@ -10,7 +10,6 @@ use { serde_with::{serde_as, DisplayFromStr}, solana_cli_output::{display::writeln_name_value, QuietDisplay, VerboseDisplay}, solana_instruction::Instruction, - solana_program_pack::Pack, solana_pubkey::Pubkey, solana_signature::Signature, solana_system_interface::instruction::transfer, @@ -18,6 +17,9 @@ use { spl_token_wrap::{ get_wrapped_mint_address, get_wrapped_mint_backpointer_address, id, instruction::create_mint, + mint_customizer::{ + default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer, + }, }, std::fmt::{Display, Formatter}, }; @@ -118,8 +120,9 @@ pub async fn command_create_mint(config: &Config, args: CreateMintArgs) -> Comma Err(_) => 0, }; + let mint_size = DefaultToken2022Customizer::get_token_2022_mint_space()?; let mint_rent = rpc_client - .get_minimum_balance_for_rent_exemption(spl_token_2022::state::Mint::LEN) + .get_minimum_balance_for_rent_exemption(mint_size) .await?; let funded_wrapped_mint_lamports = mint_rent.saturating_sub(wrapped_mint_lamports); diff --git a/clients/cli/tests/test_confidential_transfers.rs b/clients/cli/tests/test_confidential_transfers.rs new file mode 100644 index 00000000..ddd62788 --- /dev/null +++ b/clients/cli/tests/test_confidential_transfers.rs @@ -0,0 +1,68 @@ +use { + crate::helpers::{create_unwrapped_mint, execute_create_mint, setup_test_env}, + serial_test::serial, + spl_token_2022::{ + extension::{ + confidential_transfer::ConfidentialTransferMint, BaseStateWithExtensions, + PodStateWithExtensions, + }, + pod::PodMint, + }, + std::process::Command, +}; + +mod helpers; + +#[tokio::test(flavor = "multi_thread")] +#[serial] +async fn test_confidential_transfer_with_wrap_and_deposit() { + let env = setup_test_env().await; + let unwrapped_token_program = spl_token_2022::id(); + let wrapped_token_program = spl_token_2022::id(); + let unwrapped_mint = create_unwrapped_mint(&env, &unwrapped_token_program).await; + + execute_create_mint(&env, &unwrapped_mint, &wrapped_token_program).await; + let wrapped_mint_address = + spl_token_wrap::get_wrapped_mint_address(&unwrapped_mint, &wrapped_token_program); + + // Verify the wrapped mint's confidential transfer configuration + let wrapped_mint_account = env + .rpc_client + .get_account(&wrapped_mint_address) + .await + .unwrap(); + let wrapped_mint_state = + PodStateWithExtensions::::unpack(&wrapped_mint_account.data).unwrap(); + let ct_mint = wrapped_mint_state + .get_extension::() + .unwrap(); + + assert_eq!(ct_mint.authority, Default::default()); + assert!(bool::from(ct_mint.auto_approve_new_accounts)); + assert_eq!(ct_mint.auditor_elgamal_pubkey, Default::default()); + + // Create a ATA for the new wrapped mint + let create_status = Command::new("spl-token") + .args([ + "--config", + &env.config_file_path, + "create-account", + &wrapped_mint_address.to_string(), + ]) + .status() + .unwrap(); + assert!(create_status.success()); + + // Configure ATA for confidential transfers to verify confidential transfer + // extension working properly + let config_status = Command::new("spl-token") + .args([ + "--config", + &env.config_file_path, + "configure-confidential-transfer-account", + &wrapped_mint_address.to_string(), + ]) + .status() + .unwrap(); + assert!(config_status.success()); +} diff --git a/clients/cli/tests/test_create_mint.rs b/clients/cli/tests/test_create_mint.rs index d4f14d05..509e2bef 100644 --- a/clients/cli/tests/test_create_mint.rs +++ b/clients/cli/tests/test_create_mint.rs @@ -3,7 +3,13 @@ use { serial_test::serial, solana_program_pack::Pack, spl_token::{self, state::Mint as SplTokenMint}, - spl_token_2022::state::Mint as SplToken2022Mint, + spl_token_2022::{ + extension::{ + confidential_transfer::ConfidentialTransferMint, BaseStateWithExtensions, + PodStateWithExtensions, + }, + pod::PodMint, + }, spl_token_wrap::{ self, get_wrapped_mint_address, get_wrapped_mint_backpointer_address, state::Backpointer, }, @@ -44,8 +50,18 @@ async fn test_create_mint() { // Verify mint properties let unwrapped_mint_account = env.rpc_client.get_account(&unwrapped_mint).await.unwrap(); let unwrapped_mint_data = SplTokenMint::unpack(&unwrapped_mint_account.data).unwrap(); - let wrapped_mint_data = SplToken2022Mint::unpack(&wrapped_mint_account.data).unwrap(); - assert_eq!(wrapped_mint_data.decimals, unwrapped_mint_data.decimals); + let wrapped_mint_state = + PodStateWithExtensions::::unpack(&wrapped_mint_account.data).unwrap(); + assert_eq!( + wrapped_mint_state.base.decimals, + unwrapped_mint_data.decimals + ); + + // Verify confidential transfer extension is present + assert!(wrapped_mint_state + .get_extension::() + .is_ok()); + assert_eq!(wrapped_mint_state.get_extension_types().unwrap().len(), 1); // Verify backpointer data let backpointer = *bytemuck::from_bytes::(&backpointer_account.data); diff --git a/program/src/mint_customizer/default_token_2022.rs b/program/src/mint_customizer/default_token_2022.rs new file mode 100644 index 00000000..0bda9bd5 --- /dev/null +++ b/program/src/mint_customizer/default_token_2022.rs @@ -0,0 +1,56 @@ +use { + crate::mint_customizer::interface::MintCustomizer, + solana_account_info::AccountInfo, + solana_cpi::invoke, + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, + spl_token_2022::{ + extension::{ + confidential_transfer::instruction::initialize_mint as initialize_confidential_transfer_mint, + ExtensionType, PodStateWithExtensions, + }, + pod::PodMint, + state::Mint, + }, +}; + +/// This implementation adds the `ConfidentialTransferMint` extension by +/// default. +pub struct DefaultToken2022Customizer; + +impl MintCustomizer for DefaultToken2022Customizer { + fn get_token_2022_mint_space() -> Result { + let extensions = vec![ExtensionType::ConfidentialTransferMint]; + ExtensionType::try_calculate_account_len::(&extensions) + } + + fn initialize_extensions( + wrapped_mint_account: &AccountInfo, + _unwrapped_mint_account: &AccountInfo, + wrapped_token_program_account: &AccountInfo, + _all_accounts: &[AccountInfo], + ) -> ProgramResult { + invoke( + &initialize_confidential_transfer_mint( + wrapped_token_program_account.key, + wrapped_mint_account.key, + None, // Immutable. No one can later change privacy settings. + true, // No approvals necessary to use. + None, // No auditor can decrypt transaction amounts. + )?, + &[wrapped_mint_account.clone()], + ) + } + + fn get_freeze_auth_and_decimals( + 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/mint_customizer/interface.rs b/program/src/mint_customizer/interface.rs index 916c451b..646be371 100644 --- a/program/src/mint_customizer/interface.rs +++ b/program/src/mint_customizer/interface.rs @@ -8,16 +8,11 @@ use { 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; + fn get_token_2022_mint_space() -> 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, @@ -26,7 +21,6 @@ pub trait MintCustomizer { /// 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 index a855179c..b045e34e 100644 --- a/program/src/mint_customizer/mod.rs +++ b/program/src/mint_customizer/mod.rs @@ -1,5 +1,7 @@ //! Mint `customizer` interface and implementations +/// Default token 2022 mint variant +pub mod default_token_2022; /// `MintCustomizer` trait definition pub mod interface; /// No extensions version of the mint diff --git a/program/src/mint_customizer/no_extensions.rs b/program/src/mint_customizer/no_extensions.rs index a97d8639..369a5845 100644 --- a/program/src/mint_customizer/no_extensions.rs +++ b/program/src/mint_customizer/no_extensions.rs @@ -14,17 +14,12 @@ use { pub struct NoExtensionCustomizer; impl MintCustomizer for NoExtensionCustomizer { - fn get_token_2022_mint_space( - &self, - _unwrapped_mint_account: &AccountInfo, - _all_accounts: &[AccountInfo], - ) -> Result { + fn get_token_2022_mint_space() -> 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, @@ -34,7 +29,6 @@ impl MintCustomizer for NoExtensionCustomizer { } fn get_freeze_auth_and_decimals( - &self, unwrapped_mint_account: &AccountInfo, _all_accounts: &[AccountInfo], ) -> Result<(Option, u8), ProgramError> { diff --git a/program/src/processor.rs b/program/src/processor.rs index 9d493ff6..6712af88 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -8,7 +8,9 @@ use { get_wrapped_mint_backpointer_address_signer_seeds, get_wrapped_mint_backpointer_address_with_seed, get_wrapped_mint_signer_seeds, instruction::TokenWrapInstruction, - mint_customizer::{interface::MintCustomizer, no_extensions::NoExtensionCustomizer}, + mint_customizer::{ + default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer, + }, state::Backpointer, }, solana_account_info::{next_account_info, AccountInfo}, @@ -40,7 +42,6 @@ pub fn process_create_mint( program_id: &Pubkey, accounts: &[AccountInfo], idempotent: bool, - mint_customizer: M, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -101,7 +102,7 @@ pub fn process_create_mint( ); let space = if *wrapped_token_program_account.key == spl_token_2022::id() { - mint_customizer.get_token_2022_mint_space(unwrapped_mint_account, accounts)? + M::get_token_2022_mint_space()? } else { spl_token::state::Mint::get_packed_len() }; @@ -133,7 +134,7 @@ pub fn process_create_mint( // If wrapping into a token-2022 initialize extensions if *wrapped_token_program_account.key == spl_token_2022::id() { - mint_customizer.initialize_extensions( + M::initialize_extensions( wrapped_mint_account, unwrapped_mint_account, wrapped_token_program_account, @@ -142,7 +143,7 @@ pub fn process_create_mint( } let (freeze_authority, decimals) = - mint_customizer.get_freeze_auth_and_decimals(unwrapped_mint_account, accounts)?; + M::get_freeze_auth_and_decimals(unwrapped_mint_account, accounts)?; invoke( &initialize_mint2( @@ -505,7 +506,7 @@ pub fn process_instruction( // === DEVELOPER CUSTOMIZATION POINT === // To use custom mint creation logic, update the mint customizer argument msg!("Instruction: CreateMint"); - process_create_mint(program_id, accounts, idempotent, NoExtensionCustomizer) + process_create_mint::(program_id, accounts, idempotent) } TokenWrapInstruction::Wrap { amount } => { msg!("Instruction: Wrap"); diff --git a/program/tests/test_create_mint.rs b/program/tests/test_create_mint.rs index cf8da700..1ee70f32 100644 --- a/program/tests/test_create_mint.rs +++ b/program/tests/test_create_mint.rs @@ -13,7 +13,10 @@ use { solana_rent::Rent, spl_pod::primitives::{PodBool, PodU64}, spl_token_2022::{ - extension::{BaseStateWithExtensions, PodStateWithExtensions}, + extension::{ + confidential_transfer::ConfidentialTransferMint, BaseStateWithExtensions, + PodStateWithExtensions, + }, pod::PodMint, state::Mint, }, @@ -200,14 +203,20 @@ fn test_successful_spl_token_to_spl_token_2022() { // Assert state of resulting wrapped mint account assert_eq!(result.wrapped_mint.account.owner, spl_token_2022::id()); - let wrapped_mint_data = Mint::unpack(&result.wrapped_mint.account.data).unwrap(); + let wrapped_mint_data = + PodStateWithExtensions::::unpack(&result.wrapped_mint.account.data) + .unwrap() + .base; let expected_mint_authority = get_wrapped_mint_authority(&result.wrapped_mint.key); assert_eq!( - wrapped_mint_data.mint_authority.unwrap(), + wrapped_mint_data + .mint_authority + .ok_or(ProgramError::InvalidAccountData) + .unwrap(), expected_mint_authority, ); - assert_eq!(wrapped_mint_data.supply, 0); - assert!(wrapped_mint_data.is_initialized); + assert_eq!(wrapped_mint_data.supply, PodU64::from(0)); + assert_eq!(wrapped_mint_data.is_initialized, PodBool::from_bool(true)); assert_eq!( result.wrapped_backpointer.account.owner, spl_token_wrap::id() @@ -336,17 +345,17 @@ fn test_mint_customizer_copies_decimals_and_freeze_authority(from: TokenProgram, } #[test] -fn test_customizer_does_not_apply_extensions() { +fn test_customizer_applies_confidential_transfer_extension() { 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(); + let wrapped_mint_state = + PodStateWithExtensions::::unpack(&result.wrapped_mint.account.data).unwrap(); - assert_eq!(0, extensions_on_mint); + // Verify confidential transfer extension is present and is the only extension + assert!(wrapped_mint_state + .get_extension::() + .is_ok()); + assert_eq!(wrapped_mint_state.get_extension_types().unwrap().len(), 1); } diff --git a/program/tests/test_stuck_escrow.rs b/program/tests/test_stuck_escrow.rs index 462ac51c..3f713644 100644 --- a/program/tests/test_stuck_escrow.rs +++ b/program/tests/test_stuck_escrow.rs @@ -562,7 +562,12 @@ fn test_end_to_end_close_mint_case() { ( wrapped_mint_address, Account { - lamports: mollusk.sysvars.rent.minimum_balance(Mint::get_packed_len()), + lamports: mollusk.sysvars.rent.minimum_balance( + ExtensionType::try_calculate_account_len::(&[ + ExtensionType::ConfidentialTransferMint, + ]) + .unwrap(), + ), ..Default::default() }, ),