diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 110ba91..1f9d4ba 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -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 { @@ -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) +} diff --git a/program/src/lib.rs b/program/src/lib.rs index 68bf010..2e0289a 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -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, ) } @@ -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,8 +180,24 @@ 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, @@ -134,3 +205,43 @@ pub fn get_escrow_address( 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 +} diff --git a/program/src/processor.rs b/program/src/processor.rs index e2921c7..f2d3deb 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -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()?; + let mint_state = PodStateWithExtensions::::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::(); + 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::(&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) + } } } diff --git a/program/src/state.rs b/program/src/state.rs index 1533502..172bbd4 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -22,3 +22,17 @@ pub struct Backpointer { /// Address that the wrapped mint is wrapping pub unwrapped_mint: Pubkey, } + +/// An on-chain pointer to a canonical token-wrap program deployment. +/// +/// The authority of an unwrapped mint can create this account to signal which +/// deployment of the token-wrap program is the "official" one for their mint. +/// This guides users and apps especially when custom forks of the +/// program exist. +#[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, +} diff --git a/program/tests/helpers/mint_builder.rs b/program/tests/helpers/mint_builder.rs index 155b9e0..4ae84d9 100644 --- a/program/tests/helpers/mint_builder.rs +++ b/program/tests/helpers/mint_builder.rs @@ -17,7 +17,7 @@ use { pub struct MintBuilder { token_program: TokenProgram, - mint_authority: Option, + mint_authority: Option>, freeze_authority: Option, supply: u64, decimals: u8, @@ -52,7 +52,12 @@ impl MintBuilder { } pub fn mint_authority(mut self, authority: Pubkey) -> Self { - self.mint_authority = Some(authority); + self.mint_authority = Some(Some(authority)); + self + } + + pub fn no_mint_authority(mut self) -> Self { + self.mint_authority = Some(None); self } @@ -106,8 +111,11 @@ impl MintBuilder { state.base.decimals = self.decimals; state.base.is_initialized = PodBool::from_bool(true); state.base.supply = PodU64::from(self.supply); - let mint_authority = self.mint_authority.unwrap_or_else(Pubkey::new_unique); - state.base.mint_authority = PodCOption::some(mint_authority); + state.base.mint_authority = match self.mint_authority { + Some(Some(authority)) => PodCOption::some(authority), + Some(None) => PodCOption::none(), + None => PodCOption::some(Pubkey::new_unique()), // Default to random authority + }; state.base.freeze_authority = self .freeze_authority .map(PodCOption::some) diff --git a/program/tests/helpers/mod.rs b/program/tests/helpers/mod.rs index 5ca592e..0aa5e37 100644 --- a/program/tests/helpers/mod.rs +++ b/program/tests/helpers/mod.rs @@ -4,6 +4,7 @@ pub mod create_mint_builder; pub mod extensions; pub mod metadata; pub mod mint_builder; +pub mod set_canonical_pointer_builder; pub mod sync_to_spl_token_builder; pub mod sync_to_token_2022_builder; pub mod token_account_builder; diff --git a/program/tests/helpers/set_canonical_pointer_builder.rs b/program/tests/helpers/set_canonical_pointer_builder.rs new file mode 100644 index 0000000..c4f6309 --- /dev/null +++ b/program/tests/helpers/set_canonical_pointer_builder.rs @@ -0,0 +1,138 @@ +use { + crate::helpers::{ + common::{init_mollusk, KeyedAccount, TokenProgram}, + mint_builder::MintBuilder, + }, + mollusk_svm::{program::keyed_account_for_system_program, result::Check, Mollusk}, + solana_account::Account, + solana_pubkey::Pubkey, + solana_rent::Rent, + spl_token_wrap::get_canonical_pointer_address, +}; + +pub struct SetCanonicalPointerResult { + pub canonical_pointer: KeyedAccount, +} + +pub struct SetCanonicalPointerBuilder<'a> { + mollusk: Mollusk, + checks: Vec>, + unwrapped_mint_authority: Option, + is_authority_signer: bool, + canonical_pointer: Option, + unwrapped_mint: Option, + new_program_id: Option, +} + +impl Default for SetCanonicalPointerBuilder<'_> { + fn default() -> Self { + Self { + mollusk: init_mollusk(), + checks: vec![], + unwrapped_mint_authority: None, + is_authority_signer: true, + canonical_pointer: None, + unwrapped_mint: None, + new_program_id: None, + } + } +} + +impl<'a> SetCanonicalPointerBuilder<'a> { + pub fn unwrapped_mint_authority(mut self, key: Pubkey) -> Self { + self.unwrapped_mint_authority = Some(key); + self + } + + pub fn authority_not_signer(mut self) -> Self { + self.is_authority_signer = false; + self + } + + pub fn canonical_pointer(mut self, account: KeyedAccount) -> Self { + self.canonical_pointer = Some(account); + self + } + + pub fn unwrapped_mint(mut self, account: KeyedAccount) -> Self { + self.unwrapped_mint = Some(account); + self + } + + pub fn new_program_id(mut self, program_id: Pubkey) -> Self { + self.new_program_id = Some(program_id); + self + } + + pub fn check(mut self, check: Check<'a>) -> Self { + self.checks.push(check); + self + } + + pub fn execute(mut self) -> SetCanonicalPointerResult { + let unwrapped_mint_authority_key = self + .unwrapped_mint_authority + .unwrap_or_else(Pubkey::new_unique); + + let unwrapped_mint = self.unwrapped_mint.unwrap_or_else(|| { + MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(unwrapped_mint_authority_key) + .build() + }); + + let expected_pointer_address = get_canonical_pointer_address(&unwrapped_mint.key); + + let canonical_pointer = self.canonical_pointer.unwrap_or_else(|| KeyedAccount { + key: expected_pointer_address, + account: Account { + lamports: Rent::default().minimum_balance(std::mem::size_of::< + spl_token_wrap::state::CanonicalDeploymentPointer, + >()), + ..Default::default() + }, + }); + + let new_program_id = self.new_program_id.unwrap_or_else(Pubkey::new_unique); + + let unwrapped_mint_authority = KeyedAccount { + key: unwrapped_mint_authority_key, + account: Account::default(), + }; + + let mut instruction = spl_token_wrap::instruction::set_canonical_pointer( + &spl_token_wrap::id(), + &unwrapped_mint_authority.key, + &canonical_pointer.key, + &unwrapped_mint.key, + &new_program_id, + ); + + // Allow testing with non-signer authority for negative test cases + if !self.is_authority_signer { + instruction.accounts[0].is_signer = false; + } + + let accounts = &[ + unwrapped_mint_authority.pair(), + canonical_pointer.pair(), + unwrapped_mint.pair(), + keyed_account_for_system_program(), + ]; + + if self.checks.is_empty() { + self.checks.push(Check::success()); + } + + let result = + self.mollusk + .process_and_validate_instruction(&instruction, accounts, &self.checks); + + SetCanonicalPointerResult { + canonical_pointer: KeyedAccount { + key: canonical_pointer.key, + account: result.get_account(&canonical_pointer.key).unwrap().clone(), + }, + } + } +} diff --git a/program/tests/test_instruction.rs b/program/tests/test_instruction.rs index 5537662..e10b16a 100644 --- a/program/tests/test_instruction.rs +++ b/program/tests/test_instruction.rs @@ -1,4 +1,4 @@ -use spl_token_wrap::instruction::TokenWrapInstruction; +use {solana_pubkey::Pubkey, spl_token_wrap::instruction::TokenWrapInstruction}; #[test] fn test_pack_unpack_create_mint() { @@ -37,6 +37,17 @@ fn test_pack_unpack_unwrap() { assert_eq!(unpacked, instruction); } +#[test] +fn test_pack_unpack_set_canonical_pointer() { + let canonical_program_id = Pubkey::new_unique(); + let instruction = TokenWrapInstruction::SetCanonicalPointer { + program_id: canonical_program_id, + }; + let packed = instruction.pack(); + let unpacked = TokenWrapInstruction::unpack(&packed).unwrap(); + assert_eq!(unpacked, instruction); +} + #[test] fn test_unpack_invalid_data() { assert!(TokenWrapInstruction::unpack(&[]).is_err()); diff --git a/program/tests/test_set_canonical_pointer.rs b/program/tests/test_set_canonical_pointer.rs new file mode 100644 index 0000000..4d6d5b6 --- /dev/null +++ b/program/tests/test_set_canonical_pointer.rs @@ -0,0 +1,186 @@ +use { + crate::helpers::{ + common::{KeyedAccount, TokenProgram}, + mint_builder::MintBuilder, + set_canonical_pointer_builder::SetCanonicalPointerBuilder, + }, + mollusk_svm::result::Check, + solana_account::Account, + solana_program_error::ProgramError, + solana_pubkey::Pubkey, + solana_rent::Rent, + spl_token_wrap::{get_canonical_pointer_address, state::CanonicalDeploymentPointer}, +}; + +pub mod helpers; + +#[test] +fn test_fail_missing_authority_signature() { + SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(Pubkey::new_unique()) + .authority_not_signer() + .check(Check::err(ProgramError::MissingRequiredSignature)) + .execute(); +} + +#[test] +fn test_fail_invalid_mint_owner() { + let authority = Pubkey::new_unique(); + let mut mint_keyed_account = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(authority) + .build(); + mint_keyed_account.account.owner = Pubkey::new_unique(); // wrong owner + + SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(authority) + .unwrapped_mint(mint_keyed_account) + .check(Check::err(ProgramError::InvalidAccountOwner)) + .execute(); +} + +#[test] +fn test_fail_mint_has_no_authority() { + let mint_without_authority = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .no_mint_authority() + .build(); + + SetCanonicalPointerBuilder::default() + .unwrapped_mint(mint_without_authority) + .check(Check::err(ProgramError::InvalidAccountData)) + .execute(); +} + +#[test] +fn test_fail_incorrect_authority() { + let correct_authority = Pubkey::new_unique(); + let incorrect_authority = Pubkey::new_unique(); + let mint = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(correct_authority) + .build(); + + SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(incorrect_authority) + .unwrapped_mint(mint) + .check(Check::err(ProgramError::IncorrectAuthority)) + .execute(); +} + +#[test] +fn test_fail_incorrect_pointer_address() { + let authority = Pubkey::new_unique(); + let mint = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(authority) + .build(); + let incorrect_pointer = KeyedAccount { + key: Pubkey::new_unique(), // Not the derived PDA + account: Account::default(), + }; + SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(authority) + .unwrapped_mint(mint) + .canonical_pointer(incorrect_pointer) + .check(Check::err(ProgramError::InvalidArgument)) + .execute(); +} + +#[test] +fn test_fail_insufficient_funds_for_new_pointer() { + let authority = Pubkey::new_unique(); + let mint = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(authority) + .build(); + + let pointer_address = get_canonical_pointer_address(&mint.key); + let pointer_account_not_rent_exempt = KeyedAccount { + key: pointer_address, + account: Account { + lamports: Rent::default() + .minimum_balance(std::mem::size_of::()) + - 1, + ..Default::default() + }, + }; + + SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(authority) + .unwrapped_mint(mint) + .canonical_pointer(pointer_account_not_rent_exempt) + .check(Check::err(ProgramError::AccountNotRentExempt)) + .execute(); +} + +#[test] +fn test_success_create_new_pointer() { + let authority = Pubkey::new_unique(); + let mint = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(authority) + .build(); + let new_program_id = Pubkey::new_unique(); + let pointer_address = get_canonical_pointer_address(&mint.key); + let pointer_account_uninitialized = KeyedAccount { + key: pointer_address, + account: Account { + lamports: Rent::default() + .minimum_balance(std::mem::size_of::()), + ..Default::default() + }, + }; + + let result = SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(authority) + .unwrapped_mint(mint) + .canonical_pointer(pointer_account_uninitialized) + .new_program_id(new_program_id) + .execute(); + + // Check account state + assert_eq!(result.canonical_pointer.account.owner, spl_token_wrap::id()); + let pointer_data = + bytemuck::from_bytes::(&result.canonical_pointer.account.data); + assert_eq!(pointer_data.program_id, new_program_id); +} + +#[test] +fn test_success_update_existing_pointer() { + let authority = Pubkey::new_unique(); + let mint = MintBuilder::new() + .token_program(TokenProgram::SplToken) + .mint_authority(authority) + .build(); + let old_program_id = Pubkey::new_unique(); + let new_program_id = Pubkey::new_unique(); + + let pointer_address = get_canonical_pointer_address(&mint.key); + let pointer_account_initialized = KeyedAccount { + key: pointer_address, + account: Account { + lamports: Rent::default() + .minimum_balance(std::mem::size_of::()), + owner: spl_token_wrap::id(), + data: bytemuck::bytes_of(&CanonicalDeploymentPointer { + program_id: old_program_id, + }) + .to_vec(), + ..Default::default() + }, + }; + + let result = SetCanonicalPointerBuilder::default() + .unwrapped_mint_authority(authority) + .unwrapped_mint(mint) + .canonical_pointer(pointer_account_initialized) + .new_program_id(new_program_id) + .execute(); + + // Check account state + assert_eq!(result.canonical_pointer.account.owner, spl_token_wrap::id()); + let pointer_data = + bytemuck::from_bytes::(&result.canonical_pointer.account.data); + assert_eq!(pointer_data.program_id, new_program_id); +}