diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 908742dc..590a7848 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -69,18 +69,20 @@ pub enum TokenWrapInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[writeable]` Wrapped token account to unwrap - /// 1. `[writeable]` Wrapped mint, address must be: + /// 0. `[writeable]` Escrow of unwrapped tokens, must be owned by: + /// `get_wrapped_mint_authority(wrapped_mint_address)` + /// 1. `[writeable]` Recipient unwrapped tokens + /// 2. `[]` Wrapped mint authority, address must be: + /// `get_wrapped_mint_authority(wrapped_mint)` + /// 3. `[]` Unwrapped token mint + /// 4. `[]` SPL Token program for wrapped mint + /// 5. `[]` SPL Token program for unwrapped mint + /// 6. `[writeable]` Wrapped token account to unwrap + /// 7. `[writeable]` Wrapped mint, address must be: /// `get_wrapped_mint_address(unwrapped_mint_address, /// wrapped_token_program_id)` - /// 2. `[writeable]` Escrow of unwrapped tokens, must be owned by: - /// `get_wrapped_mint_authority(wrapped_mint_address)` - /// 3. `[writeable]` Recipient unwrapped tokens - /// 4. `[]` Unwrapped token mint - /// 5. `[]` SPL Token program for wrapped mint - /// 6. `[]` SPL Token program for unwrapped mint - /// 7. `[signer]` Transfer authority on wrapped token account - /// 8. `..8+M` `[signer]` (Optional) M multisig signers on wrapped token + /// 8. `[signer]` Transfer authority on wrapped token account + /// 9. `..8+M` `[signer]` (Optional) M multisig signers on wrapped token /// account Unwrap { /// little-endian `u64` representing the amount to unwrap @@ -198,25 +200,27 @@ pub fn wrap( #[allow(clippy::too_many_arguments)] pub fn unwrap( program_id: &Pubkey, - wrapped_token_account_address: &Pubkey, - wrapped_mint_address: &Pubkey, unwrapped_escrow_address: &Pubkey, recipient_unwrapped_token_account_address: &Pubkey, + wrapped_mint_authority_address: &Pubkey, unwrapped_mint_address: &Pubkey, wrapped_token_program_id: &Pubkey, unwrapped_token_program_id: &Pubkey, + wrapped_token_account_address: &Pubkey, + wrapped_mint_address: &Pubkey, transfer_authority_address: &Pubkey, multisig_signer_pubkeys: &[&Pubkey], amount: u64, ) -> Instruction { let mut accounts = vec![ - AccountMeta::new(*wrapped_token_account_address, false), - AccountMeta::new(*wrapped_mint_address, false), AccountMeta::new(*unwrapped_escrow_address, false), AccountMeta::new(*recipient_unwrapped_token_account_address, false), + AccountMeta::new_readonly(*wrapped_mint_authority_address, false), AccountMeta::new_readonly(*unwrapped_mint_address, false), AccountMeta::new_readonly(*wrapped_token_program_id, false), AccountMeta::new_readonly(*unwrapped_token_program_id, false), + AccountMeta::new(*wrapped_token_account_address, false), + AccountMeta::new(*wrapped_mint_address, false), AccountMeta::new_readonly( *transfer_authority_address, multisig_signer_pubkeys.is_empty(), diff --git a/program/src/processor.rs b/program/src/processor.rs index daa677b1..c51108b0 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -164,7 +164,7 @@ pub fn process_create_mint( } /// Processes [`Wrap`](enum.TokenWrapInstruction.html) instruction. -pub fn process_wrap(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult { +pub fn process_wrap(accounts: &[AccountInfo], amount: u64) -> ProgramResult { if amount == 0 { Err(TokenWrapError::ZeroWrapAmount)? } @@ -251,6 +251,87 @@ pub fn process_wrap(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) Ok(()) } +/// Processes [`Unwrap`](enum.TokenWrapInstruction.html) instruction. +pub fn process_unwrap(accounts: &[AccountInfo], amount: u64) -> ProgramResult { + if amount == 0 { + Err(TokenWrapError::ZeroWrapAmount)? + } + + let account_info_iter = &mut accounts.iter(); + + let unwrapped_escrow = next_account_info(account_info_iter)?; + let recipient_unwrapped_token = next_account_info(account_info_iter)?; + let wrapped_mint_authority = next_account_info(account_info_iter)?; + let unwrapped_mint = next_account_info(account_info_iter)?; + let wrapped_token_program = next_account_info(account_info_iter)?; + let unwrapped_token_program = next_account_info(account_info_iter)?; + let wrapped_token_account = next_account_info(account_info_iter)?; + let wrapped_mint = next_account_info(account_info_iter)?; + let transfer_authority = next_account_info(account_info_iter)?; + let multisig_signer_accounts = account_info_iter.as_slice(); + + // Validate accounts + + let expected_wrapped_mint = + get_wrapped_mint_address(unwrapped_mint.key, wrapped_token_program.key); + if expected_wrapped_mint != *wrapped_mint.key { + Err(TokenWrapError::WrappedMintMismatch)? + } + + let (expected_authority, bump) = get_wrapped_mint_authority_with_seed(wrapped_mint.key); + if *wrapped_mint_authority.key != expected_authority { + Err(TokenWrapError::MintAuthorityMismatch)? + } + + // Burn wrapped tokens + + let multisig_signer_pubkeys = multisig_signer_accounts + .iter() + .map(|account| account.key) + .collect::>(); + + invoke( + &spl_token_2022::instruction::burn( + wrapped_token_program.key, + wrapped_token_account.key, + wrapped_mint.key, + transfer_authority.key, + &multisig_signer_pubkeys, + amount, + )?, + &accounts[6..], + )?; + + // Transfer unwrapped tokens from escrow to recipient + + let unwrapped_mint_data = unwrapped_mint.try_borrow_data()?; + let unwrapped_mint_state = PodStateWithExtensions::::unpack(&unwrapped_mint_data)?; + let bump_seed = [bump]; + let signer_seeds = get_wrapped_mint_authority_signer_seeds(wrapped_mint.key, &bump_seed); + + invoke_signed( + &spl_token_2022::instruction::transfer_checked( + unwrapped_token_program.key, + unwrapped_escrow.key, + unwrapped_mint.key, + recipient_unwrapped_token.key, + wrapped_mint_authority.key, + &[], + amount, + unwrapped_mint_state.base.decimals, + )?, + &[ + unwrapped_escrow.clone(), + unwrapped_mint.clone(), + recipient_unwrapped_token.clone(), + wrapped_mint_authority.clone(), + ], + &[&signer_seeds], + )?; + + Ok(()) +} + /// Instruction processor pub fn process_instruction( program_id: &Pubkey, @@ -264,11 +345,11 @@ pub fn process_instruction( } TokenWrapInstruction::Wrap { amount } => { msg!("Instruction: Wrap"); - process_wrap(program_id, accounts, amount) + process_wrap(accounts, amount) } - TokenWrapInstruction::Unwrap { .. } => { + TokenWrapInstruction::Unwrap { amount } => { msg!("Instruction: Unwrap"); - unimplemented!(); + process_unwrap(accounts, amount) } } } diff --git a/program/tests/helpers/mod.rs b/program/tests/helpers/mod.rs index 6e386387..744aba6c 100644 --- a/program/tests/helpers/mod.rs +++ b/program/tests/helpers/mod.rs @@ -1,3 +1,4 @@ pub mod common; pub mod create_mint_builder; +pub mod unwrap_builder; pub mod wrap_builder; diff --git a/program/tests/helpers/unwrap_builder.rs b/program/tests/helpers/unwrap_builder.rs new file mode 100644 index 00000000..bccaf13e --- /dev/null +++ b/program/tests/helpers/unwrap_builder.rs @@ -0,0 +1,299 @@ +use { + crate::helpers::{ + common::{init_mollusk, setup_mint}, + create_mint_builder::{KeyedAccount, TokenProgram}, + wrap_builder::TransferAuthority, + }, + mollusk_svm::{result::Check, Mollusk}, + solana_account::Account, + solana_pubkey::Pubkey, + spl_token_2022::{ + extension::{ + transfer_fee::TransferFeeAmount, BaseStateWithExtensionsMut, ExtensionType, + PodStateWithExtensionsMut, + }, + pod::{PodAccount, PodCOption}, + }, + spl_token_wrap::{get_wrapped_mint_address, get_wrapped_mint_authority, instruction::unwrap}, +}; + +pub struct UnwrapBuilder<'a> { + mollusk: Mollusk, + unwrap_amount: Option, + checks: Vec>, + wrapped_mint: Option, + wrapped_mint_authority: Option, + escrow_starting_amount: Option, + unwrapped_escrow_owner: Option, + wrapped_token_starting_amount: Option, + recipient_starting_amount: Option, + unwrapped_token_program: Option, + wrapped_token_program: Option, + transfer_authority: Option, +} + +impl Default for UnwrapBuilder<'_> { + fn default() -> Self { + Self { + mollusk: init_mollusk(), + unwrap_amount: None, + checks: vec![], + wrapped_mint: None, + wrapped_mint_authority: None, + escrow_starting_amount: None, + unwrapped_escrow_owner: None, + wrapped_token_starting_amount: None, + recipient_starting_amount: None, + unwrapped_token_program: None, + wrapped_token_program: None, + transfer_authority: None, + } + } +} + +impl<'a> UnwrapBuilder<'a> { + pub fn unwrap_amount(mut self, amount: u64) -> Self { + self.unwrap_amount = Some(amount); + self + } + + pub fn wrapped_token_starting_amount(mut self, amount: u64) -> Self { + self.wrapped_token_starting_amount = Some(amount); + self + } + + pub fn escrow_starting_amount(mut self, amount: u64) -> Self { + self.escrow_starting_amount = Some(amount); + self + } + + pub fn unwrapped_escrow_owner(mut self, key: Pubkey) -> Self { + self.unwrapped_escrow_owner = Some(key); + self + } + + pub fn wrapped_mint(mut self, account: KeyedAccount) -> Self { + self.wrapped_mint = Some(account); + self + } + + pub fn wrapped_token_program(mut self, program: TokenProgram) -> Self { + self.wrapped_token_program = Some(program); + self + } + + pub fn unwrapped_token_program(mut self, program: TokenProgram) -> Self { + self.unwrapped_token_program = Some(program); + self + } + + pub fn wrapped_mint_authority(mut self, key: Pubkey) -> Self { + self.wrapped_mint_authority = Some(key); + self + } + + pub fn recipient_starting_amount(mut self, amount: u64) -> Self { + self.recipient_starting_amount = Some(amount); + self + } + + pub fn transfer_authority(mut self, auth: TransferAuthority) -> Self { + self.transfer_authority = Some(auth); + self + } + + pub fn check(mut self, check: Check<'a>) -> Self { + self.checks.push(check); + self + } + + fn get_wrapped_mint( + &self, + token_program: TokenProgram, + unwrapped_mint_addr: Pubkey, + ) -> KeyedAccount { + let wrapped_mint_addr = get_wrapped_mint_address(&unwrapped_mint_addr, &token_program.id()); + let mint_authority = get_wrapped_mint_authority(&wrapped_mint_addr); + + self.wrapped_mint.clone().unwrap_or(KeyedAccount { + key: wrapped_mint_addr, + account: setup_mint(token_program, &self.mollusk.sysvars.rent, mint_authority), + }) + } + + pub fn setup_token_account( + &self, + token_program: TokenProgram, + mint: &KeyedAccount, + owner: &Pubkey, + starting_amount: u64, + ) -> KeyedAccount { + let extensions = match token_program { + TokenProgram::SplToken => vec![], + TokenProgram::SplToken2022 => vec![ExtensionType::TransferFeeAmount], + }; + + let account_size = + ExtensionType::try_calculate_account_len::(&extensions).unwrap(); + + let mut token_account = Account { + lamports: 100_000_000, + owner: mint.account.owner, + data: vec![0; account_size], + ..Default::default() + }; + + let account_data = PodAccount { + mint: mint.key, + owner: *owner, + amount: starting_amount.into(), + delegate: PodCOption::none(), + state: spl_token_2022::state::AccountState::Initialized.into(), + is_native: PodCOption::none(), + delegated_amount: 0.into(), + close_authority: PodCOption::none(), + }; + + let mut state = + PodStateWithExtensionsMut::::unpack_uninitialized(&mut token_account.data) + .unwrap(); + *state.base = account_data; + state.init_account_type().unwrap(); + + if let TokenProgram::SplToken2022 = token_program { + state.init_extension::(true).unwrap(); + let fee_extension = state.get_extension_mut::().unwrap(); + fee_extension.withheld_amount = 12.into(); + } + + KeyedAccount { + key: Pubkey::new_unique(), + account: token_account, + } + } + + pub fn execute(mut self) -> UnwrapResult { + let unwrap_amount = self.unwrap_amount.unwrap_or(500); + let transfer_authority = self.transfer_authority.clone().unwrap_or_default(); + + let unwrapped_token_program = self + .unwrapped_token_program + .unwrap_or(TokenProgram::SplToken); + + let unwrapped_mint = KeyedAccount { + key: Pubkey::new_unique(), + account: setup_mint( + unwrapped_token_program, + &self.mollusk.sysvars.rent, + Pubkey::new_unique(), + ), + }; + + let wrapped_token_program = self + .wrapped_token_program + .unwrap_or(TokenProgram::SplToken2022); + + let wrapped_mint = self + .wrapped_mint + .clone() + .unwrap_or_else(|| self.get_wrapped_mint(wrapped_token_program, unwrapped_mint.key)); + + let wrapped_mint_authority = self + .wrapped_mint_authority + .unwrap_or_else(|| get_wrapped_mint_authority(&wrapped_mint.key)); + + // Setup wrapped token account to be unwrapped + let wrapped_token_account = self.setup_token_account( + wrapped_token_program, + &wrapped_mint, + &transfer_authority.keyed_account.key, + self.wrapped_token_starting_amount.unwrap_or(unwrap_amount), + ); + + // Setup escrow account + let escrow = self.setup_token_account( + unwrapped_token_program, + &unwrapped_mint, + &self + .unwrapped_escrow_owner + .unwrap_or(wrapped_mint_authority), + self.escrow_starting_amount.unwrap_or(100_000), + ); + + // Setup recipient account for unwrapped tokens + let recipient = self.setup_token_account( + unwrapped_token_program, + &unwrapped_mint, + &Pubkey::new_unique(), + self.recipient_starting_amount.unwrap_or(0), + ); + + let instruction = unwrap( + &spl_token_wrap::id(), + &escrow.key, + &recipient.key, + &wrapped_mint_authority, + &unwrapped_mint.key, + &wrapped_token_program.id(), + &unwrapped_token_program.id(), + &wrapped_token_account.key, + &wrapped_mint.key, + &transfer_authority.keyed_account.key, + &transfer_authority.signers.iter().collect::>(), + unwrap_amount, + ); + + let mut accounts = vec![ + escrow.pair(), + recipient.pair(), + (wrapped_mint_authority, Account::default()), + unwrapped_mint.pair(), + wrapped_token_program.keyed_account(), + unwrapped_token_program.keyed_account(), + wrapped_token_account.pair(), + wrapped_mint.pair(), + transfer_authority.keyed_account.pair(), + ]; + + for signer_key in &transfer_authority.signers { + accounts.push((*signer_key, Account::default())); + } + + if self.checks.is_empty() { + self.checks.push(Check::success()); + } + + let result = + self.mollusk + .process_and_validate_instruction(&instruction, &accounts, &self.checks); + + UnwrapResult { + wrapped_token_account: KeyedAccount { + key: wrapped_token_account.key, + account: result + .get_account(&wrapped_token_account.key) + .unwrap() + .clone(), + }, + unwrapped_escrow: KeyedAccount { + key: escrow.key, + account: result.get_account(&escrow.key).unwrap().clone(), + }, + wrapped_mint: KeyedAccount { + key: wrapped_mint.key, + account: result.get_account(&wrapped_mint.key).unwrap().clone(), + }, + recipient_unwrapped_token: KeyedAccount { + key: recipient.key, + account: result.get_account(&recipient.key).unwrap().clone(), + }, + } + } +} + +pub struct UnwrapResult { + pub wrapped_token_account: KeyedAccount, + pub unwrapped_escrow: KeyedAccount, + pub wrapped_mint: KeyedAccount, + pub recipient_unwrapped_token: KeyedAccount, +} diff --git a/program/tests/test_unwrap.rs b/program/tests/test_unwrap.rs new file mode 100644 index 00000000..b8ee6685 --- /dev/null +++ b/program/tests/test_unwrap.rs @@ -0,0 +1,247 @@ +use { + crate::helpers::{ + common::{setup_multisig, MINT_SUPPLY}, + create_mint_builder::{CreateMintBuilder, KeyedAccount, TokenProgram}, + unwrap_builder::{UnwrapBuilder, UnwrapResult}, + }, + mollusk_svm::result::Check, + solana_pubkey::Pubkey, + spl_token_2022::{ + error::TokenError, + extension::PodStateWithExtensions, + pod::{PodAccount, PodMint}, + }, + spl_token_wrap::error::TokenWrapError, +}; + +pub mod helpers; + +#[test] +fn test_zero_amount_unwrap() { + UnwrapBuilder::default() + .unwrap_amount(0) + .check(Check::err(TokenWrapError::ZeroWrapAmount.into())) + .execute(); +} + +#[test] +fn test_incorrect_wrapped_mint_address() { + let mint_result = CreateMintBuilder::default().execute(); + + let incorrect_wrapped_mint = KeyedAccount { + key: Pubkey::new_unique(), // Wrong mint address + account: mint_result.wrapped_mint.account.clone(), + }; + + UnwrapBuilder::default() + .wrapped_mint(incorrect_wrapped_mint) + .check(Check::err(TokenWrapError::WrappedMintMismatch.into())) + .execute(); +} + +#[test] +fn test_incorrect_wrapped_mint_authority() { + let incorrect_authority = Pubkey::new_unique(); + UnwrapBuilder::default() + .wrapped_mint_authority(incorrect_authority) + .check(Check::err(TokenWrapError::MintAuthorityMismatch.into())) + .execute(); +} + +#[test] +fn test_unwrap_amount_exceeds_unwrappers_balance() { + let wrapped_balance = 1_000; + let unwrap_amount = 42_000; + + UnwrapBuilder::default() + .wrapped_token_starting_amount(wrapped_balance) + .unwrap_amount(unwrap_amount) + .check(Check::err(TokenError::InsufficientFunds.into())) + .execute(); +} + +fn assert_unwrap_result( + source_starting_amount: u64, + recipient_starting_amount: u64, + escrow_starting_amount: u64, + unwrap_amount: u64, + unwrap_result: &UnwrapResult, +) { + // Verify wrapped tokens were burned (source account) + let wrapped_token = PodStateWithExtensions::::unpack( + &unwrap_result.wrapped_token_account.account.data, + ) + .unwrap(); + assert_eq!( + wrapped_token.base.amount, + source_starting_amount + .checked_sub(unwrap_amount) + .unwrap() + .into() + ); + + // Verify wrapped mint supply decreased + let mint = PodStateWithExtensions::::unpack(&unwrap_result.wrapped_mint.account.data) + .unwrap(); + assert_eq!( + u64::from(mint.base.supply), + MINT_SUPPLY.checked_sub(unwrap_amount).unwrap() + ); + + // Verify escrow was debited + let escrow_token = + PodStateWithExtensions::::unpack(&unwrap_result.unwrapped_escrow.account.data) + .unwrap(); + assert_eq!( + u64::from(escrow_token.base.amount), + escrow_starting_amount.checked_sub(unwrap_amount).unwrap() + ); + + // Verify recipient received unwrapped tokens + let recipient_token = PodStateWithExtensions::::unpack( + &unwrap_result.recipient_unwrapped_token.account.data, + ) + .unwrap(); + assert_eq!( + u64::from(recipient_token.base.amount), + recipient_starting_amount + .checked_add(unwrap_amount) + .unwrap() + ); +} + +#[test] +fn test_successful_spl_token_2022_to_spl_token_unwrap() { + let source_starting_amount = 50_000; + let recipient_starting_amount = 50_000; + let escrow_starting_amount = 150_000; + let unwrap_amount = 12_555; + + let wrap_result = UnwrapBuilder::default() + .wrapped_token_starting_amount(source_starting_amount) + .unwrapped_token_program(TokenProgram::SplToken) + .wrapped_token_program(TokenProgram::SplToken2022) + .escrow_starting_amount(escrow_starting_amount) + .recipient_starting_amount(recipient_starting_amount) + .unwrap_amount(unwrap_amount) + .check(Check::success()) + .execute(); + + assert_unwrap_result( + source_starting_amount, + recipient_starting_amount, + escrow_starting_amount, + unwrap_amount, + &wrap_result, + ); +} + +#[test] +fn test_successful_spl_token_to_spl_token_2022_unwrap() { + let source_starting_amount = 50_000; + let recipient_starting_amount = 25_000; + let escrow_starting_amount = 42_000; + let unwrap_amount = 40_000; + + let wrap_result = UnwrapBuilder::default() + .unwrapped_token_program(TokenProgram::SplToken2022) + .wrapped_token_program(TokenProgram::SplToken) + .escrow_starting_amount(escrow_starting_amount) + .wrapped_token_starting_amount(source_starting_amount) + .recipient_starting_amount(recipient_starting_amount) + .unwrap_amount(unwrap_amount) + .check(Check::success()) + .execute(); + + assert_unwrap_result( + source_starting_amount, + recipient_starting_amount, + escrow_starting_amount, + unwrap_amount, + &wrap_result, + ); +} + +#[test] +fn test_successful_token_2022_to_token_2022_unwrap() { + let source_starting_amount = 150_000; + let recipient_starting_amount = 0; + let escrow_starting_amount = 100_000; + let unwrap_amount = 100_000; + + let wrap_result = UnwrapBuilder::default() + .unwrapped_token_program(TokenProgram::SplToken2022) + .wrapped_token_program(TokenProgram::SplToken2022) + .escrow_starting_amount(escrow_starting_amount) + .wrapped_token_starting_amount(source_starting_amount) + .recipient_starting_amount(recipient_starting_amount) + .unwrap_amount(unwrap_amount) + .check(Check::success()) + .execute(); + + assert_unwrap_result( + source_starting_amount, + recipient_starting_amount, + escrow_starting_amount, + unwrap_amount, + &wrap_result, + ); +} + +#[test] +fn test_unwrap_with_spl_token_multisig() { + let multisig = setup_multisig(TokenProgram::SplToken); + + let source_starting_amount = 100_000; + let recipient_starting_amount = 0; + let escrow_starting_amount = 100_000; + let unwrap_amount = 100_000; + + let wrap_result = UnwrapBuilder::default() + .transfer_authority(multisig) + .unwrapped_token_program(TokenProgram::SplToken2022) + .wrapped_token_program(TokenProgram::SplToken) + .escrow_starting_amount(escrow_starting_amount) + .wrapped_token_starting_amount(source_starting_amount) + .recipient_starting_amount(recipient_starting_amount) + .unwrap_amount(unwrap_amount) + .check(Check::success()) + .execute(); + + assert_unwrap_result( + source_starting_amount, + recipient_starting_amount, + escrow_starting_amount, + unwrap_amount, + &wrap_result, + ); +} + +#[test] +fn test_unwrap_with_spl_token_2022_multisig() { + let multisig = setup_multisig(TokenProgram::SplToken2022); + + let source_starting_amount = 101; + let recipient_starting_amount = 101; + let escrow_starting_amount = 202; + let unwrap_amount = 101; + + let wrap_result = UnwrapBuilder::default() + .transfer_authority(multisig) + .unwrapped_token_program(TokenProgram::SplToken) + .wrapped_token_program(TokenProgram::SplToken2022) + .escrow_starting_amount(escrow_starting_amount) + .wrapped_token_starting_amount(source_starting_amount) + .recipient_starting_amount(recipient_starting_amount) + .unwrap_amount(unwrap_amount) + .check(Check::success()) + .execute(); + + assert_unwrap_result( + source_starting_amount, + recipient_starting_amount, + escrow_starting_amount, + unwrap_amount, + &wrap_result, + ); +}