-
Notifications
You must be signed in to change notification settings - Fork 8
Introduce canonical pointer PDA #374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Take it or leave it, but I find the name
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can update 👍 |
||
| /// The program ID to set as canonical | ||
| program_id: Pubkey, | ||
| }, | ||
| } | ||
|
|
||
| impl TokenWrapInstruction { | ||
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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), | ||
| } | ||
| } | ||
|
|
@@ -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) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tbh I'd just have them all just take If this is easier for SemVer, though, nbd.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
|
|
@@ -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"; | ||
|
|
@@ -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"; | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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, | ||
|
|
@@ -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. | ||
|
|
@@ -793,6 +794,91 @@ 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); | ||
| } | ||
|
|
||
| if unwrapped_mint_info.owner != &spl_token::id() | ||
| && unwrapped_mint_info.owner != &spl_token_2022::id() | ||
| { | ||
| return Err(ProgramError::InvalidAccountOwner); | ||
| } | ||
|
|
||
| let mint_data = unwrapped_mint_info.try_borrow_data()?; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -826,5 +912,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) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we confirm that this is ok from the foundation? If an SPL token mint is using an SPL token multisig as the authority, we can't actually get a signature for it 😅
USDC and USDT both use SPL token multisigs as the mint and freeze authority, so we might need to add SPL multisig support to this instruction, which may require rejiggering the order of accounts here.
Separately, what if we allowed for a signature from either the freeze authority or the mint authority?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you talking about the feature generally? In NY, they specifically requested a way for a mint authority to signal another token-wrap deployment was the canonical one for the mint.
Oh shoot. Totally forgot that multisigs cannot directly sign txs. Yes, I'll need to reorder the accounts for that usecase like the other ixs.
Yes, that sounds reasonable too.