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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
mod entrypoint;
pub mod error;
pub mod instruction;
pub mod mint_customizer;
pub mod processor;
pub mod state;

Expand Down
33 changes: 33 additions & 0 deletions program/src/mint_customizer/interface.rs
Original file line number Diff line number Diff line change
@@ -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<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,
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<Pubkey>, u8), ProgramError>;
}
6 changes: 6 additions & 0 deletions program/src/mint_customizer/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upcoming PRs will add additional mint customizers here

48 changes: 48 additions & 0 deletions program/src/mint_customizer/no_extensions.rs
Original file line number Diff line number Diff line change
@@ -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<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,
_all_accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

fn get_freeze_auth_and_decimals(
&self,
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))
}
}
48 changes: 31 additions & 17 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -33,10 +36,11 @@ use {
};

/// Processes [`CreateMint`](enum.TokenWrapInstruction.html) instruction.
pub fn process_create_mint(
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 @@ -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);
Expand All @@ -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::<PodMint>::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,
Expand Down Expand Up @@ -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");
Expand Down
63 changes: 45 additions & 18 deletions program/tests/test_create_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -202,21 +201,13 @@ 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(),
expected_mint_authority,
);
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()
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -323,3 +307,46 @@ fn test_create_mint_from_extended_mint(extension: MintExtension) {
bytemuck::from_bytes::<Backpointer>(&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::<PodMint>::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::<PodMint>::unpack(&result.wrapped_mint.account.data)
.unwrap()
.get_extension_types()
.unwrap()
.len();

assert_eq!(0, extensions_on_mint);
}