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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ level = "warn"
check-cfg = [
'cfg(target_os, values("solana"))',
'cfg(feature, values("frozen-abi", "no-entrypoint"))',
'cfg(feature, values("custom-heap", "custom-panic"))',
Copy link
Member Author

Choose a reason for hiding this comment

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

New lint warnings require this

]

[workspace.package]
Expand Down Expand Up @@ -77,6 +78,7 @@ spl-token-2022 = { version = "9.0.0", features = ["no-entrypoint"] }
spl-token-metadata-interface = "0.7.0"
spl-token-wrap = { path = "program", features = ["no-entrypoint"] }
spl-transfer-hook-interface = "0.10.0"
spl-type-length-value = "0.8.0"
tempfile = "3.20.0"
test-case = "3.3.1"
test-transfer-hook = { path = "program/tests/programs/test-transfer-hook", features = ["no-entrypoint"] }
Expand Down
7 changes: 2 additions & 5 deletions clients/cli/src/create_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ use {
solana_transaction::Transaction,
spl_token::solana_program::program_pack::Pack,
spl_token_wrap::{
get_wrapped_mint_address, get_wrapped_mint_authority, get_wrapped_mint_backpointer_address,
id,
get_wrapped_mint_address, get_wrapped_mint_backpointer_address, id,
instruction::create_mint,
mint_customizer::{
default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer,
Expand Down Expand Up @@ -123,7 +122,7 @@ pub async fn command_create_mint(config: &Config, args: CreateMintArgs) -> Comma
};

let mint_size = if args.wrapped_token_program == spl_token_2022::id() {
DefaultToken2022Customizer::get_token_2022_total_space()?
DefaultToken2022Customizer::get_token_2022_mint_space()?
} else {
spl_token::state::Mint::LEN
};
Expand Down Expand Up @@ -177,13 +176,11 @@ pub async fn command_create_mint(config: &Config, args: CreateMintArgs) -> Comma
}

// Add the create_mint instruction
let wrapped_mint_authority_address = get_wrapped_mint_authority(&wrapped_mint_address);
instructions.push(create_mint(
&id(),
&wrapped_mint_address,
&wrapped_backpointer_address,
&args.unwrapped_mint,
&wrapped_mint_authority_address,
&args.wrapped_token_program,
args.idempotent,
));
Expand Down
13 changes: 1 addition & 12 deletions clients/cli/tests/test_create_mint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use {
},
pod::PodMint,
},
spl_token_metadata_interface::state::TokenMetadata,
spl_token_wrap::{
self, get_wrapped_mint_address, get_wrapped_mint_authority,
get_wrapped_mint_backpointer_address, state::Backpointer,
Expand Down Expand Up @@ -65,7 +64,7 @@ async fn test_create_mint() {
assert_eq!(backpointer.unwrapped_mint, unwrapped_mint);

// Verify extension state
assert_eq!(wrapped_mint_state.get_extension_types().unwrap().len(), 3);
assert_eq!(wrapped_mint_state.get_extension_types().unwrap().len(), 2);

assert!(wrapped_mint_state
.get_extension::<ConfidentialTransferMint>()
Expand All @@ -84,14 +83,4 @@ async fn test_create_mint() {
Option::<Pubkey>::from(pointer_ext.metadata_address).unwrap(),
wrapped_mint_address
);

// Verify TokenMetadata content
let metadata_ext = wrapped_mint_state
.get_variable_len_extension::<TokenMetadata>()
.unwrap();
assert_eq!(
Option::<Pubkey>::from(metadata_ext.update_authority).unwrap(),
expected_mint_authority
);
assert_eq!(metadata_ext.mint, wrapped_mint_address);
}
1 change: 1 addition & 0 deletions program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ spl-token-2022 = { workspace = true }
mollusk-svm = { workspace = true }
mollusk-svm-programs-token = { workspace = true }
solana-account = { workspace = true }
spl-type-length-value = { workspace = true }
spl-tlv-account-resolution = { workspace = true }
test-case = { workspace = true }
test-transfer-hook = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions program/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub enum TokenWrapError {
/// The escrow account is in a good state and cannot be recreated
#[error("The escrow account is in a good state and cannot be recreated")]
EscrowInGoodState,
/// Unwrapped mint does not have the `TokenMetadata` extension
#[error("Unwrapped mint does not have the TokenMetadata extension")]
UnwrappedMintHasNoMetadata,
}

impl From<TokenWrapError> for ProgramError {
Expand Down Expand Up @@ -73,6 +76,7 @@ impl ToStr for TokenWrapError {
TokenWrapError::InvalidBackpointerOwner => "Error: InvalidBackpointerOwner",
TokenWrapError::EscrowMismatch => "Error: EscrowMismatch",
TokenWrapError::EscrowInGoodState => "Error: EscrowInGoodState",
TokenWrapError::UnwrappedMintHasNoMetadata => "Error: UnwrappedMintHasNoMetadata",
}
}
}
Expand Down
44 changes: 42 additions & 2 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ pub enum TokenWrapInstruction {
/// 4. `[]` Wrapped mint authority (PDA)
/// 5. `[]` Token-2022 program
CloseStuckEscrow,

/// This instruction copies the metadata fields from an unwrapped mint to
/// its wrapped mint `TokenMetadata` extension.
///
/// Supports (unwrapped to wrapped):
/// - Token-2022 to Token-2022
/// - SPL-token to Token-2022 (still `TODO`)
///
/// If the `TokenMetadata` extension on the wrapped mint if not present, it
/// will initialize it. The client is responsible for funding the wrapped
/// mint account with enough lamports to cover the rent for the
/// additional space required by the `TokenMetadata` extension and/or
/// metadata sync.
///
/// Accounts expected by this instruction:
///
/// 0. `[w]` Wrapped mint
/// 1. `[]` Wrapped mint authority (PDA)
/// 2. `[]` Unwrapped mint
/// 3. `[]` Token-2022 program
SyncMetadataToToken2022,
}

impl TokenWrapInstruction {
Expand All @@ -132,6 +153,9 @@ impl TokenWrapInstruction {
TokenWrapInstruction::CloseStuckEscrow => {
buf.push(3);
}
TokenWrapInstruction::SyncMetadataToToken2022 => {
buf.push(4);
}
}
buf
}
Expand All @@ -157,6 +181,7 @@ impl TokenWrapInstruction {
Ok(TokenWrapInstruction::Unwrap { amount })
}
Some((&3, [])) => Ok(TokenWrapInstruction::CloseStuckEscrow),
Some((&4, [])) => Ok(TokenWrapInstruction::SyncMetadataToToken2022),
_ => Err(ProgramError::InvalidInstructionData),
}
}
Expand All @@ -168,15 +193,13 @@ pub fn create_mint(
wrapped_mint_address: &Pubkey,
wrapped_backpointer_address: &Pubkey,
unwrapped_mint_address: &Pubkey,
wrapped_mint_authority_address: &Pubkey,
wrapped_token_program_id: &Pubkey,
idempotent: bool,
) -> Instruction {
let accounts = vec![
AccountMeta::new(*wrapped_mint_address, false),
AccountMeta::new(*wrapped_backpointer_address, false),
AccountMeta::new_readonly(*unwrapped_mint_address, false),
AccountMeta::new_readonly(*wrapped_mint_authority_address, false),
AccountMeta::new_readonly(solana_system_interface::program::id(), false),
AccountMeta::new_readonly(*wrapped_token_program_id, false),
];
Expand Down Expand Up @@ -280,3 +303,20 @@ pub fn close_stuck_escrow(
let data = TokenWrapInstruction::CloseStuckEscrow.pack();
Instruction::new_with_bytes(*program_id, &data, accounts)
}

/// Creates `SyncMetadataToToken2022` instruction.
pub fn sync_metadata_to_token_2022(
program_id: &Pubkey,
wrapped_mint: &Pubkey,
wrapped_mint_authority: &Pubkey,
unwrapped_mint: &Pubkey,
) -> Instruction {
let accounts = vec![
AccountMeta::new(*wrapped_mint, false),
AccountMeta::new_readonly(*wrapped_mint_authority, false),
AccountMeta::new_readonly(*unwrapped_mint, false),
AccountMeta::new_readonly(spl_token_2022::id(), false),
];
let data = TokenWrapInstruction::SyncMetadataToToken2022.pack();
Instruction::new_with_bytes(*program_id, &data, accounts)
}
55 changes: 4 additions & 51 deletions program/src/mint_customizer/default_token_2022.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
crate::{get_wrapped_mint_authority, mint_customizer::interface::MintCustomizer},
solana_account_info::AccountInfo,
solana_cpi::{invoke, invoke_signed},
solana_cpi::invoke,
solana_program_error::{ProgramError, ProgramResult},
solana_pubkey::Pubkey,
spl_token_2022::{
Expand All @@ -14,17 +14,14 @@ use {
pod::PodMint,
state::Mint,
},
spl_token_metadata_interface::{
instruction::initialize as initialize_token_metadata, state::TokenMetadata,
},
};

/// This implementation adds the `ConfidentialTransferMint` & `TokenMetadata`
/// This implementation adds the `ConfidentialTransferMint` & `MetadataPointer`
/// extensions by default.
pub struct DefaultToken2022Customizer;

impl MintCustomizer for DefaultToken2022Customizer {
fn get_token_2022_mint_initialization_space() -> Result<usize, ProgramError> {
fn get_token_2022_mint_space() -> Result<usize, ProgramError> {
// Calculate space for all extensions that are initialized *before* the base
// mint. The TokenMetadata extension is initialized *after* and its
// `initialize` instruction handles its own reallocation.
Expand All @@ -34,15 +31,7 @@ impl MintCustomizer for DefaultToken2022Customizer {
])
}

fn get_token_2022_total_space() -> Result<usize, ProgramError> {
let base_size = Self::get_token_2022_mint_initialization_space()?;
let metadata_size = TokenMetadata::default().tlv_size_of()?;
base_size
.checked_add(metadata_size)
.ok_or(ProgramError::ArithmeticOverflow)
}

fn pre_initialize_extensions(
fn initialize_extensions(
wrapped_mint_account: &AccountInfo,
wrapped_token_program_account: &AccountInfo,
) -> ProgramResult {
Expand Down Expand Up @@ -73,42 +62,6 @@ impl MintCustomizer for DefaultToken2022Customizer {
Ok(())
}

fn post_initialize_extensions<'a>(
wrapped_mint_account: &AccountInfo<'a>,
wrapped_token_program_account: &AccountInfo,
wrapped_mint_authority_account: &AccountInfo<'a>,
mint_authority_signer_seeds: &[&[u8]],
) -> ProgramResult {
// Initialize metadata ext (must be done after mint initialization)
let wrapped_mint_authority = get_wrapped_mint_authority(wrapped_mint_account.key);

let cpi_accounts = [
wrapped_mint_account.clone(),
wrapped_mint_authority_account.clone(),
wrapped_mint_account.clone(),
wrapped_mint_authority_account.clone(),
];

invoke_signed(
&initialize_token_metadata(
wrapped_token_program_account.key,
wrapped_mint_account.key,
&wrapped_mint_authority,
wrapped_mint_account.key,
&wrapped_mint_authority,
// Initialized as empty, but separate instructions are available
// to update these fields
"".to_string(),
"".to_string(),
"".to_string(),
),
&cpi_accounts,
&[mint_authority_signer_seeds],
)?;
Comment on lines -92 to -107
Copy link
Member Author

Choose a reason for hiding this comment

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

Removing this saves rent on those wrapped mints without unwrapped mint metadata


Ok(())
}

fn get_freeze_auth_and_decimals(
unwrapped_mint_account: &AccountInfo,
) -> Result<(Option<Pubkey>, u8), ProgramError> {
Expand Down
31 changes: 7 additions & 24 deletions program/src/mint_customizer/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,17 @@ use {
pub trait MintCustomizer {
/// Calculates the space required for a new spl-token-2022 mint
/// account, including any custom extensions
fn get_token_2022_mint_initialization_space() -> Result<usize, ProgramError>;

/// Calculates the total space required for a new spl-token-2022
/// mint, after `post_initialize_extensions()` is called. This is useful in
/// calculating rent requirements if something like `TokenMetadata` does a
/// realloc after the mint is created. If not implemented, defaults to
/// `get_token_2022_mint_initialization_space()` result.
fn get_token_2022_total_space() -> Result<usize, ProgramError> {
Self::get_token_2022_mint_initialization_space()
}
fn get_token_2022_mint_space() -> Result<usize, ProgramError>;

/// Customizes extensions for the wrapped mint *before* the base mint is
/// initialized. This is for extensions that must be initialized on an
/// uninitialized mint account, like `ConfidentialTransferMint`.
fn pre_initialize_extensions(
wrapped_mint_account: &AccountInfo,
wrapped_token_program_account: &AccountInfo,
) -> ProgramResult;

/// Customizes extensions for the wrapped mint *after* the base mint is
/// initialized. This is for extensions that require the mint to be
/// initialized, like `TokenMetadata`.
fn post_initialize_extensions<'a>(
wrapped_mint_account: &AccountInfo<'a>,
wrapped_token_program_account: &AccountInfo,
wrapped_mint_authority_account: &AccountInfo<'a>,
mint_authority_signer_seeds: &[&[u8]],
) -> ProgramResult;
Comment on lines -25 to -38
Copy link
Member Author

Choose a reason for hiding this comment

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

Simplifying this interface and removing the pre-post-mint-init pattern here (given we don't do anything post init in CreateMint anymore)

fn initialize_extensions(
_wrapped_mint_account: &AccountInfo,
_wrapped_token_program_account: &AccountInfo,
) -> ProgramResult {
Ok(())
}

/// Customize the freeze authority and decimals for the wrapped mint
fn get_freeze_auth_and_decimals(
Expand Down
20 changes: 2 additions & 18 deletions program/src/mint_customizer/no_extensions.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use {
crate::mint_customizer::interface::MintCustomizer,
solana_account_info::AccountInfo,
solana_program_error::{ProgramError, ProgramResult},
solana_program_error::ProgramError,
solana_pubkey::Pubkey,
spl_token_2022::{
extension::{ExtensionType, PodStateWithExtensions},
Expand All @@ -14,27 +14,11 @@ use {
pub struct NoExtensionCustomizer;

impl MintCustomizer for NoExtensionCustomizer {
fn get_token_2022_mint_initialization_space() -> Result<usize, ProgramError> {
fn get_token_2022_mint_space() -> Result<usize, ProgramError> {
let extensions = vec![];
ExtensionType::try_calculate_account_len::<Mint>(&extensions)
}

fn pre_initialize_extensions(
_wrapped_mint_account: &AccountInfo,
_wrapped_token_program_account: &AccountInfo,
) -> ProgramResult {
Ok(())
}

fn post_initialize_extensions<'a>(
_wrapped_mint_account: &AccountInfo<'a>,
_wrapped_token_program_account: &AccountInfo,
_wrapped_mint_authority_account: &AccountInfo<'a>,
_mint_authority_signer_seeds: &[&[u8]],
) -> ProgramResult {
Ok(())
}

fn get_freeze_auth_and_decimals(
unwrapped_mint_account: &AccountInfo,
) -> Result<(Option<Pubkey>, u8), ProgramError> {
Expand Down
Loading