Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 |
Expand Down
7 changes: 5 additions & 2 deletions clients/cli/src/create_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ 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,
solana_transaction::Transaction,
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},
};
Expand Down Expand Up @@ -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);
Expand Down
68 changes: 68 additions & 0 deletions clients/cli/tests/test_confidential_transfers.rs
Original file line number Diff line number Diff line change
@@ -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::<PodMint>::unpack(&wrapped_mint_account.data).unwrap();
let ct_mint = wrapped_mint_state
.get_extension::<ConfidentialTransferMint>()
.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());
}
22 changes: 19 additions & 3 deletions clients/cli/tests/test_create_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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::<PodMint>::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::<ConfidentialTransferMint>()
.is_ok());
assert_eq!(wrapped_mint_state.get_extension_types().unwrap().len(), 1);

// Verify backpointer data
let backpointer = *bytemuck::from_bytes::<Backpointer>(&backpointer_account.data);
Expand Down
56 changes: 56 additions & 0 deletions program/src/mint_customizer/default_token_2022.rs
Original file line number Diff line number Diff line change
@@ -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<usize, ProgramError> {
let extensions = vec![ExtensionType::ConfidentialTransferMint];
ExtensionType::try_calculate_account_len::<Mint>(&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<Pubkey>, u8), ProgramError> {
// Copy fields over from original mint
let unwrapped_mint_data = unwrapped_mint_account.try_borrow_data()?;
let pod_mint = PodStateWithExtensions::<PodMint>::unpack(&unwrapped_mint_data)?.base;
let freeze_authority = pod_mint.freeze_authority.ok_or(()).ok();
let decimals = pod_mint.decimals;
Ok((freeze_authority, decimals))
}
}
8 changes: 1 addition & 7 deletions program/src/mint_customizer/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize, ProgramError>;
fn get_token_2022_mint_space() -> Result<usize, ProgramError>;

/// 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,
Expand All @@ -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<Pubkey>, u8), ProgramError>;
Expand Down
2 changes: 2 additions & 0 deletions program/src/mint_customizer/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 1 addition & 7 deletions program/src/mint_customizer/no_extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize, ProgramError> {
fn get_token_2022_mint_space() -> Result<usize, ProgramError> {
let extensions = vec![];
ExtensionType::try_calculate_account_len::<Mint>(&extensions)
}

fn initialize_extensions(
&self,
_wrapped_mint_account: &AccountInfo,
_unwrapped_mint_account: &AccountInfo,
_wrapped_token_program_account: &AccountInfo,
Expand All @@ -34,7 +29,6 @@ impl MintCustomizer for NoExtensionCustomizer {
}

fn get_freeze_auth_and_decimals(
&self,
unwrapped_mint_account: &AccountInfo,
_all_accounts: &[AccountInfo],
) -> Result<(Option<Pubkey>, u8), ProgramError> {
Expand Down
13 changes: 7 additions & 6 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -40,7 +42,6 @@ pub fn process_create_mint<M: MintCustomizer>(
program_id: &Pubkey,
accounts: &[AccountInfo],
idempotent: bool,
mint_customizer: M,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();

Expand Down Expand Up @@ -101,7 +102,7 @@ pub fn process_create_mint<M: MintCustomizer>(
);

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()
};
Expand Down Expand Up @@ -133,7 +134,7 @@ pub fn process_create_mint<M: MintCustomizer>(

// 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,
Expand All @@ -142,7 +143,7 @@ pub fn process_create_mint<M: MintCustomizer>(
}

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(
Expand Down Expand Up @@ -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::<DefaultToken2022Customizer>(program_id, accounts, idempotent)
}
TokenWrapInstruction::Wrap { amount } => {
msg!("Instruction: Wrap");
Expand Down
Loading