Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
56 changes: 56 additions & 0 deletions program/src/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,33 @@ pub enum TokenWrapInstruction {
/// 8. `[]` (Optional) Owner program. Required when metadata account is
/// owned by a third-party program.
SyncMetadataToSplToken,

/// Creates or updates the canonical program pointer for a mint.
///
/// The mint authority of an unwrapped mint may desire to deploy a forked
/// version of the Token Wrap program themselves. It's likely the case they
/// prefer a certain set of extensions or a particular config for the
/// wrapped token-2022s. They may even freeze the unwrapped mint's
/// escrow account in the original deployment to force the use of the fork.
/// A `CanonicalPointer` PDA allows a mint authority to signal on-chain
/// another Token Wrap deployment is the "canonical" one for the mint.
///
/// If calling for the first time, the client is responsible for pre-funding
/// the rent for the PDA that will be initialized.
///
/// If no mint authority exists on the unwrapped mint, this instruction will
/// fail.
///
/// Accounts expected:
/// 0. `[s]` Unwrapped mint authority
/// 1. `[w]` `CanonicalPointer` PDA account to create or update, address
/// must be: `get_canonical_pointer_address(unwrapped_mint_address)`
/// 2. `[]` Unwrapped mint
/// 3. `[]` System program
SetCanonicalPointer {
/// The program ID to set as canonical
program_id: Pubkey,
},
}

impl TokenWrapInstruction {
Expand Down Expand Up @@ -200,6 +227,10 @@ impl TokenWrapInstruction {
TokenWrapInstruction::SyncMetadataToSplToken => {
buf.push(5);
}
TokenWrapInstruction::SetCanonicalPointer { program_id } => {
buf.push(6);
buf.extend_from_slice(program_id.as_ref());
}
}
buf
}
Expand Down Expand Up @@ -227,6 +258,10 @@ impl TokenWrapInstruction {
Some((&3, [])) => Ok(TokenWrapInstruction::CloseStuckEscrow),
Some((&4, [])) => Ok(TokenWrapInstruction::SyncMetadataToToken2022),
Some((&5, [])) => Ok(TokenWrapInstruction::SyncMetadataToSplToken),
Some((&6, rest)) if rest.len() == 32 => {
let program_id = Pubkey::new_from_array(rest.try_into().unwrap());
Ok(TokenWrapInstruction::SetCanonicalPointer { program_id })
}
_ => Err(ProgramError::InvalidInstructionData),
}
}
Expand Down Expand Up @@ -408,3 +443,24 @@ pub fn sync_metadata_to_spl_token(
let data = TokenWrapInstruction::SyncMetadataToSplToken.pack();
Instruction::new_with_bytes(*program_id, &data, accounts)
}

/// Creates `SetCanonicalPointer` instruction.
pub fn set_canonical_pointer(
program_id: &Pubkey,
mint_authority: &Pubkey,
pointer_address: &Pubkey,
unwrapped_mint: &Pubkey,
canonical_program_id: &Pubkey,
) -> Instruction {
let accounts = vec![
AccountMeta::new_readonly(*mint_authority, true),
AccountMeta::new(*pointer_address, false),
AccountMeta::new_readonly(*unwrapped_mint, false),
AccountMeta::new_readonly(solana_system_interface::program::id(), false),
];
let data = TokenWrapInstruction::SetCanonicalPointer {
program_id: *canonical_program_id,
}
.pack();
Instruction::new_with_bytes(*program_id, &data, accounts)
}
127 changes: 119 additions & 8 deletions program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@ const WRAPPED_MINT_SEED: &[u8] = br"mint";
pub(crate) fn get_wrapped_mint_address_with_seed(
unwrapped_mint: &Pubkey,
wrapped_token_program_id: &Pubkey,
) -> (Pubkey, u8) {
get_wrapped_mint_address_with_seed_for_program(unwrapped_mint, wrapped_token_program_id, &id())
}

pub(crate) fn get_wrapped_mint_address_with_seed_for_program(
unwrapped_mint: &Pubkey,
wrapped_token_program_id: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&get_wrapped_mint_seeds(unwrapped_mint, wrapped_token_program_id),
&id(),
program_id,
)
}
Comment on lines 23 to 39
Copy link
Member Author

Choose a reason for hiding this comment

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

Adding a _for_program() variants to allow for these forks to utilize our package for their address lookups

Choose a reason for hiding this comment

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

Tbh I'd just have them all just take program_id and not support duplicate versions of all of the helpers.

If this is easier for SemVer, though, nbd.

Copy link
Member Author

Choose a reason for hiding this comment

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

id() is so deeply embedded into the program+clients that requiring the program_id is a far reaching change. I think I'll keep this forward compatible for now and revisit for a later breaking change if we find lots of folks forking and wanting the simpler name.


Expand Down Expand Up @@ -59,7 +67,22 @@ pub fn get_wrapped_mint_address(
unwrapped_mint: &Pubkey,
wrapped_token_program_id: &Pubkey,
) -> Pubkey {
get_wrapped_mint_address_with_seed(unwrapped_mint, wrapped_token_program_id).0
get_wrapped_mint_address_for_program(unwrapped_mint, wrapped_token_program_id, &id())
}

/// Derive the SPL Token wrapped mint address associated with an unwrapped mint
/// for a specific Token Wrap program deployment.
pub fn get_wrapped_mint_address_for_program(
unwrapped_mint: &Pubkey,
wrapped_token_program_id: &Pubkey,
program_id: &Pubkey,
) -> Pubkey {
get_wrapped_mint_address_with_seed_for_program(
unwrapped_mint,
wrapped_token_program_id,
program_id,
)
.0
}

const WRAPPED_MINT_AUTHORITY_SEED: &[u8] = br"authority";
Expand All @@ -80,12 +103,28 @@ pub(crate) fn get_wrapped_mint_authority_signer_seeds<'a>(
}

pub(crate) fn get_wrapped_mint_authority_with_seed(wrapped_mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&get_wrapped_mint_authority_seeds(wrapped_mint), &id())
get_wrapped_mint_authority_with_seed_for_program(wrapped_mint, &id())
}

pub(crate) fn get_wrapped_mint_authority_with_seed_for_program(
wrapped_mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(&get_wrapped_mint_authority_seeds(wrapped_mint), program_id)
}

/// Derive the SPL Token wrapped mint authority address
pub fn get_wrapped_mint_authority(wrapped_mint: &Pubkey) -> Pubkey {
get_wrapped_mint_authority_with_seed(wrapped_mint).0
get_wrapped_mint_authority_for_program(wrapped_mint, &id())
}

/// Derive the SPL Token wrapped mint authority address for a specific Token
/// Wrap program deployment
pub fn get_wrapped_mint_authority_for_program(
wrapped_mint: &Pubkey,
program_id: &Pubkey,
) -> Pubkey {
get_wrapped_mint_authority_with_seed_for_program(wrapped_mint, program_id).0
}

const WRAPPED_MINT_BACKPOINTER_SEED: &[u8] = br"backpointer";
Expand All @@ -107,16 +146,32 @@ pub(crate) fn get_wrapped_mint_backpointer_address_signer_seeds<'a>(

pub(crate) fn get_wrapped_mint_backpointer_address_with_seed(
wrapped_mint: &Pubkey,
) -> (Pubkey, u8) {
get_wrapped_mint_backpointer_address_with_seed_for_program(wrapped_mint, &id())
}

pub(crate) fn get_wrapped_mint_backpointer_address_with_seed_for_program(
wrapped_mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&get_wrapped_mint_backpointer_address_seeds(wrapped_mint),
&id(),
program_id,
)
}

/// Derive the SPL Token wrapped mint backpointer address
pub fn get_wrapped_mint_backpointer_address(wrapped_mint: &Pubkey) -> Pubkey {
get_wrapped_mint_backpointer_address_with_seed(wrapped_mint).0
get_wrapped_mint_backpointer_address_for_program(wrapped_mint, &id())
}

/// Derive the SPL Token wrapped mint backpointer address for a specific Token
/// Wrap program deployment.
pub fn get_wrapped_mint_backpointer_address_for_program(
wrapped_mint: &Pubkey,
program_id: &Pubkey,
) -> Pubkey {
get_wrapped_mint_backpointer_address_with_seed_for_program(wrapped_mint, program_id).0
}

/// Derive the escrow `ATA` that backs a given wrapped mint.
Expand All @@ -125,12 +180,68 @@ pub fn get_escrow_address(
unwrapped_token_program_id: &Pubkey,
wrapped_token_program_id: &Pubkey,
) -> Pubkey {
let wrapped_mint = get_wrapped_mint_address(unwrapped_mint, wrapped_token_program_id);
let mint_authority = get_wrapped_mint_authority(&wrapped_mint);
get_escrow_address_for_program(
unwrapped_mint,
unwrapped_token_program_id,
wrapped_token_program_id,
&id(),
)
}

/// Derive the escrow `ATA` for a specific Token Wrap program deployment.
pub fn get_escrow_address_for_program(
unwrapped_mint: &Pubkey,
unwrapped_token_program_id: &Pubkey,
wrapped_token_program_id: &Pubkey,
program_id: &Pubkey,
) -> Pubkey {
let wrapped_mint =
get_wrapped_mint_address_for_program(unwrapped_mint, wrapped_token_program_id, program_id);
let mint_authority = get_wrapped_mint_authority_for_program(&wrapped_mint, program_id);

get_associated_token_address_with_program_id(
&mint_authority,
unwrapped_mint,
unwrapped_token_program_id,
)
}

const CANONICAL_POINTER_SEED: &[u8] = br"canonical_pointer";

/// Derives the canonical pointer address and bump seed for a specific
/// Token Wrap program deployment.
pub(crate) fn get_canonical_pointer_address_with_seed_for_program(
unwrapped_mint: &Pubkey,
program_id: &Pubkey,
) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[CANONICAL_POINTER_SEED, unwrapped_mint.as_ref()],
program_id,
)
}

pub(crate) fn get_canonical_pointer_address_signer_seeds<'a>(
unwrapped_mint: &'a Pubkey,
bump_seed: &'a [u8],
) -> [&'a [u8]; 3] {
[CANONICAL_POINTER_SEED, unwrapped_mint.as_ref(), bump_seed]
}

/// Derives the canonical pointer address and bump seed.
pub(crate) fn get_canonical_pointer_address_with_seed(unwrapped_mint: &Pubkey) -> (Pubkey, u8) {
get_canonical_pointer_address_with_seed_for_program(unwrapped_mint, &id())
}

/// Derives the canonical pointer address for an unwrapped mint.
pub fn get_canonical_pointer_address(unwrapped_mint: &Pubkey) -> Pubkey {
get_canonical_pointer_address_for_program(unwrapped_mint, &id())
}

/// Derives the canonical pointer address for an unwrapped mint for a specific
/// Token Wrap program deployment.
pub fn get_canonical_pointer_address_for_program(
unwrapped_mint: &Pubkey,
program_id: &Pubkey,
) -> Pubkey {
get_canonical_pointer_address_with_seed_for_program(unwrapped_mint, program_id).0
}
90 changes: 88 additions & 2 deletions program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use {
crate::{
error::TokenWrapError,
get_canonical_pointer_address_signer_seeds, get_canonical_pointer_address_with_seed,
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,
Expand All @@ -13,7 +14,7 @@ use {
mint_customizer::{
default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer,
},
state::Backpointer,
state::{Backpointer, CanonicalDeploymentPointer},
},
mpl_token_metadata::{
accounts::Metadata as MetaplexMetadata,
Expand Down Expand Up @@ -48,7 +49,7 @@ use {
instruction::{initialize as initialize_token_metadata, remove_key, update_field},
state::{Field, TokenMetadata},
},
std::collections::HashMap,
std::{collections::HashMap, mem},
};

/// Processes [`CreateMint`](enum.TokenWrapInstruction.html) instruction.
Expand Down Expand Up @@ -793,6 +794,85 @@ pub fn process_sync_metadata_to_spl_token(accounts: &[AccountInfo]) -> ProgramRe
Ok(())
}

/// Processes [`SetCanonicalPointer`](enum.TokenWrapInstruction.html)
/// instruction.
pub fn process_set_canonical_pointer(
program_id: &Pubkey,
accounts: &[AccountInfo],
new_program_id: Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let unwrapped_mint_authority_info = next_account_info(account_info_iter)?;
let canonical_pointer_info = next_account_info(account_info_iter)?;
let unwrapped_mint_info = next_account_info(account_info_iter)?;
let _system_program_info = next_account_info(account_info_iter)?;

if !unwrapped_mint_authority_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}

let mint_data = unwrapped_mint_info.try_borrow_data()?;

Choose a reason for hiding this comment

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

Should we be checking the mint's owner here? Maybe it's inconsequential, since the mint address can't be duplicated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure thing, will add a check+test

let mint_state = PodStateWithExtensions::<PodMint>::unpack(&mint_data)?;
let mint_authority = mint_state
.base
.mint_authority
.ok_or(ProgramError::InvalidAccountData)
.inspect_err(|_| {
msg!("Cannot create/update pointer for unwrapped mint if does not have an authority");
})?;

if mint_authority != *unwrapped_mint_authority_info.key {
return Err(ProgramError::IncorrectAuthority);
}

let (expected_pointer_address, bump) =
get_canonical_pointer_address_with_seed(unwrapped_mint_info.key);
if *canonical_pointer_info.key != expected_pointer_address {
msg!(
"Error: canonical pointer address {} does not match expected address {}",
canonical_pointer_info.key,
expected_pointer_address
);
return Err(ProgramError::InvalidArgument);
}

// If pointer does not exist, initialize it
if canonical_pointer_info.data_is_empty() {
let space = mem::size_of::<CanonicalDeploymentPointer>();
let rent_required = Rent::get()?.minimum_balance(space);

if canonical_pointer_info.lamports() < rent_required {
msg!(
"Error: canonical pointer PDA requires pre-funding of {} lamports",
rent_required
);
Err(ProgramError::AccountNotRentExempt)?
}

let bump_seed = [bump];
let signer_seeds =
get_canonical_pointer_address_signer_seeds(unwrapped_mint_info.key, &bump_seed);
invoke_signed(
&allocate(canonical_pointer_info.key, space as u64),
&[canonical_pointer_info.clone()],
&[&signer_seeds],
)?;
invoke_signed(
&assign(canonical_pointer_info.key, program_id),
&[canonical_pointer_info.clone()],
&[&signer_seeds],
)?;
}

// Set data within canonical pointer PDA

let mut pointer_data = canonical_pointer_info.try_borrow_mut_data()?;
let state = bytemuck::from_bytes_mut::<CanonicalDeploymentPointer>(&mut pointer_data);
state.program_id = new_program_id;

Ok(())
}

/// Instruction processor
pub fn process_instruction(
program_id: &Pubkey,
Expand Down Expand Up @@ -826,5 +906,11 @@ pub fn process_instruction(
msg!("Instruction: SyncMetadataToSplToken");
process_sync_metadata_to_spl_token(accounts)
}
TokenWrapInstruction::SetCanonicalPointer {
program_id: new_program_id,
} => {
msg!("Instruction: SetCanonicalPointer");
process_set_canonical_pointer(program_id, accounts, new_program_id)
}
}
}
10 changes: 10 additions & 0 deletions program/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ pub struct Backpointer {
/// Address that the wrapped mint is wrapping
pub unwrapped_mint: Pubkey,
}

/// The mint authority of an unwrapped mint's on-chain signal another token-wrap
/// deployment is the "canonical" one.

Choose a reason for hiding this comment

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

nit: this comment is a little bit confusing. Do you think we can clarify it a bit?

#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
#[repr(C)]
pub struct CanonicalDeploymentPointer {
/// The program ID of the canonical token-wrap deployment as determined by
/// the unwrapped mint authority.
pub program_id: Pubkey,
}
Loading