From b30b8a2c4e0aa98496918597d5008d75c27e1090 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 14 Feb 2025 10:48:13 +0100 Subject: [PATCH] `Wrap` multisig support (#11) Wrap multisig support --- program/src/instruction.rs | 32 ++++++------ program/src/processor.rs | 26 +++++----- program/tests/helpers/common.rs | 46 ++++++++++++++++- program/tests/helpers/create_mint_builder.rs | 2 +- program/tests/helpers/wrap_builder.rs | 54 ++++++++++++++------ program/tests/test_wrap.rs | 33 +++++++++++- 6 files changed, 144 insertions(+), 49 deletions(-) diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 2c56591..908742d 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -40,19 +40,19 @@ pub enum TokenWrapInstruction { /// /// Accounts expected by this instruction: /// - /// 0. `[w]` Escrow of unwrapped tokens, must be owned by: - /// `get_wrapped_mint_authority(wrapped_mint_address)` - /// 1. `[w]` Unwrapped token account to wrap - /// `get_wrapped_mint_authority(wrapped_mint_address)` - /// 2. `[w]` Recipient wrapped token account - /// 3. `[w]` Wrapped mint, must be initialized, address must be: + /// 0. `[w]` Recipient wrapped token account + /// 1. `[w]` Wrapped mint, must be initialized, address must be: /// `get_wrapped_mint_address(unwrapped_mint_address, /// wrapped_token_program_id)` - /// 4. `[]` Unwrapped token mint - /// 5. `[]` Wrapped mint authority, address must be: + /// 2. `[]` Wrapped mint authority, address must be: /// `get_wrapped_mint_authority(wrapped_mint)` - /// 6. `[]` SPL Token program for unwrapped mint - /// 7. `[]` SPL Token program for wrapped mint + /// 3. `[]` SPL Token program for unwrapped mint + /// 4. `[]` SPL Token program for wrapped mint + /// 5. `[w]` Unwrapped token account to wrap + /// `get_wrapped_mint_authority(wrapped_mint_address)` + /// 6. `[]` Unwrapped token mint + /// 7. `[w]` Escrow of unwrapped tokens, must be owned by: + /// `get_wrapped_mint_authority(wrapped_mint_address)` /// 8. `[s]` Transfer authority on unwrapped token account. Not required to /// be a signer if it's a multisig. /// 9. `..8+M` `[s]` (Optional) M multisig signers on unwrapped token @@ -160,27 +160,27 @@ pub fn create_mint( #[allow(clippy::too_many_arguments)] pub fn wrap( program_id: &Pubkey, - unwrapped_escrow_address: &Pubkey, - unwrapped_token_account_address: &Pubkey, recipient_wrapped_token_account_address: &Pubkey, wrapped_mint_address: &Pubkey, - unwrapped_mint_address: &Pubkey, wrapped_mint_authority_address: &Pubkey, unwrapped_token_program_id: &Pubkey, wrapped_token_program_id: &Pubkey, + unwrapped_token_account_address: &Pubkey, + unwrapped_mint_address: &Pubkey, + unwrapped_escrow_address: &Pubkey, transfer_authority_address: &Pubkey, multisig_signer_pubkeys: &[&Pubkey], amount: u64, ) -> Instruction { let mut accounts = vec![ - AccountMeta::new(*unwrapped_escrow_address, false), - AccountMeta::new(*unwrapped_token_account_address, false), AccountMeta::new(*recipient_wrapped_token_account_address, false), AccountMeta::new(*wrapped_mint_address, false), - AccountMeta::new_readonly(*unwrapped_mint_address, false), AccountMeta::new_readonly(*wrapped_mint_authority_address, false), AccountMeta::new_readonly(*unwrapped_token_program_id, false), AccountMeta::new_readonly(*wrapped_token_program_id, false), + AccountMeta::new(*unwrapped_token_account_address, false), + AccountMeta::new_readonly(*unwrapped_mint_address, false), + AccountMeta::new(*unwrapped_escrow_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 2cad4f0..daa677b 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -171,16 +171,16 @@ pub fn process_wrap(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) let account_info_iter = &mut accounts.iter(); - let unwrapped_escrow = next_account_info(account_info_iter)?; - let unwrapped_token_account = next_account_info(account_info_iter)?; let recipient_wrapped_token_account = next_account_info(account_info_iter)?; let wrapped_mint = next_account_info(account_info_iter)?; - let unwrapped_mint = next_account_info(account_info_iter)?; let wrapped_mint_authority = next_account_info(account_info_iter)?; let unwrapped_token_program = next_account_info(account_info_iter)?; let wrapped_token_program = next_account_info(account_info_iter)?; + let unwrapped_token_account = next_account_info(account_info_iter)?; + let unwrapped_mint = next_account_info(account_info_iter)?; + let unwrapped_escrow = next_account_info(account_info_iter)?; let transfer_authority = next_account_info(account_info_iter)?; - let _signer_accounts = account_info_iter.as_slice(); + let multisig_signer_accounts = account_info_iter.as_slice(); // Validate accounts @@ -207,24 +207,24 @@ pub fn process_wrap(_program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) let unwrapped_mint_data = unwrapped_mint.try_borrow_data()?; let unwrapped_mint_state = PodStateWithExtensions::::unpack(&unwrapped_mint_data)?; - invoke_signed( + + let multisig_signer_pubkeys = multisig_signer_accounts + .iter() + .map(|account| account.key) + .collect::>(); + + invoke( &spl_token_2022::instruction::transfer_checked( unwrapped_token_program.key, unwrapped_token_account.key, unwrapped_mint.key, unwrapped_escrow.key, transfer_authority.key, - &[], + &multisig_signer_pubkeys, amount, unwrapped_mint_state.base.decimals, )?, - &[ - unwrapped_token_account.clone(), - unwrapped_mint.clone(), - unwrapped_escrow.clone(), - transfer_authority.clone(), - ], - &[], + &accounts[5..], )?; // Mint wrapped tokens to recipient diff --git a/program/tests/helpers/common.rs b/program/tests/helpers/common.rs index 921e92d..cf9d531 100644 --- a/program/tests/helpers/common.rs +++ b/program/tests/helpers/common.rs @@ -1,5 +1,8 @@ use { - crate::helpers::create_mint_builder::TokenProgram, + crate::helpers::{ + create_mint_builder::{KeyedAccount, TokenProgram}, + wrap_builder::TransferAuthority, + }, mollusk_svm::Mollusk, solana_account::Account, solana_program_option::COption, @@ -89,3 +92,44 @@ pub fn setup_mint(token_program: TokenProgram, rent: &Rent, mint_authority: Pubk ..Default::default() } } + +pub fn setup_multisig(program: TokenProgram) -> TransferAuthority { + let multisig_key = Pubkey::new_unique(); + let signer0_key = Pubkey::new_unique(); + let signer1_key = Pubkey::new_unique(); + let signer2_key = Pubkey::new_unique(); + + let mut multisig_account = Account { + lamports: 100_000_000, + owner: program.id(), + data: vec![0; spl_token_2022::state::Multisig::LEN], + ..Account::default() + }; + + let multisig_state = spl_token_2022::state::Multisig { + m: 2, + n: 3, + is_initialized: true, + signers: [ + signer0_key, + signer1_key, + signer2_key, + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + ], + }; + spl_token_2022::state::Multisig::pack(multisig_state, &mut multisig_account.data).unwrap(); + TransferAuthority { + keyed_account: KeyedAccount { + key: multisig_key, + account: multisig_account, + }, + signers: vec![signer0_key, signer1_key, signer2_key], + } +} diff --git a/program/tests/helpers/create_mint_builder.rs b/program/tests/helpers/create_mint_builder.rs index 2c696f5..ccedd7b 100644 --- a/program/tests/helpers/create_mint_builder.rs +++ b/program/tests/helpers/create_mint_builder.rs @@ -15,7 +15,7 @@ pub struct CreateMintResult { pub wrapped_backpointer: KeyedAccount, } -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct KeyedAccount { pub key: Pubkey, pub account: Account, diff --git a/program/tests/helpers/wrap_builder.rs b/program/tests/helpers/wrap_builder.rs index b661683..cc8ea8e 100644 --- a/program/tests/helpers/wrap_builder.rs +++ b/program/tests/helpers/wrap_builder.rs @@ -17,6 +17,12 @@ use { spl_token_wrap::{get_wrapped_mint_address, get_wrapped_mint_authority, instruction::wrap}, }; +#[derive(Debug, Clone, Default)] +pub struct TransferAuthority { + pub keyed_account: KeyedAccount, + pub signers: Vec, +} + pub struct WrapBuilder<'a> { mollusk: Mollusk, wrap_amount: Option, @@ -31,6 +37,7 @@ pub struct WrapBuilder<'a> { unwrapped_escrow_account: Option, unwrapped_token_program: Option, wrapped_token_program: Option, + transfer_authority: Option, } impl Default for WrapBuilder<'_> { @@ -49,6 +56,7 @@ impl Default for WrapBuilder<'_> { unwrapped_escrow_account: None, unwrapped_token_program: None, wrapped_token_program: None, + transfer_authority: None, } } } @@ -104,6 +112,11 @@ impl<'a> WrapBuilder<'a> { 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 @@ -178,7 +191,7 @@ impl<'a> WrapBuilder<'a> { pub fn execute(mut self) -> WrapResult { let unwrapped_token_account_address = Pubkey::new_unique(); - let unwrapped_token_account_authority = Pubkey::new_unique(); + let unwrapped_token_account_authority = self.transfer_authority.clone().unwrap_or_default(); let unwrapped_token_program = self .unwrapped_token_program @@ -204,7 +217,7 @@ impl<'a> WrapBuilder<'a> { let token = spl_token::state::Account { mint: unwrapped_mint.key, - owner: unwrapped_token_account_authority, + owner: unwrapped_token_account_authority.keyed_account.key, amount: self.unwrapped_token_starting_amount.unwrap_or(wrap_amount), delegate: None.into(), state: spl_token::state::AccountState::Initialized, @@ -258,42 +271,49 @@ impl<'a> WrapBuilder<'a> { let instruction = wrap( &spl_token_wrap::id(), - &unwrapped_escrow_address, - &unwrapped_token_account_address, &recipient.key, &wrapped_mint.key, - &unwrapped_mint.key, &wrapped_mint_authority, &unwrapped_token_program.id(), &wrapped_token_program.id(), - &unwrapped_token_account_authority, - &[], + &unwrapped_token_account_address, + &unwrapped_mint.key, + &unwrapped_escrow_address, + &unwrapped_token_account_authority.keyed_account.key, + &unwrapped_token_account_authority + .signers + .iter() + .collect::>(), wrap_amount, ); - let accounts = &[ - ( - unwrapped_escrow_address, - self.unwrapped_escrow_account - .unwrap_or(unwrapped_escrow_account), - ), - (unwrapped_token_account_address, unwrapped_token_account), + let mut accounts = vec![ recipient.pair(), wrapped_mint.pair(), - unwrapped_mint.pair(), (wrapped_mint_authority, Account::default()), unwrapped_token_program.keyed_account(), wrapped_token_program.keyed_account(), - (unwrapped_token_account_authority, Account::default()), + (unwrapped_token_account_address, unwrapped_token_account), + unwrapped_mint.pair(), + ( + unwrapped_escrow_address, + self.unwrapped_escrow_account + .unwrap_or(unwrapped_escrow_account), + ), + unwrapped_token_account_authority.keyed_account.pair(), ]; + for signer_key in &unwrapped_token_account_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); + .process_and_validate_instruction(&instruction, &accounts, &self.checks); WrapResult { unwrapped_token: KeyedAccount { diff --git a/program/tests/test_wrap.rs b/program/tests/test_wrap.rs index 0111b5e..29c4999 100644 --- a/program/tests/test_wrap.rs +++ b/program/tests/test_wrap.rs @@ -1,6 +1,6 @@ use { crate::helpers::{ - common::MINT_SUPPLY, + common::{setup_multisig, MINT_SUPPLY}, create_mint_builder::{CreateMintBuilder, KeyedAccount, TokenProgram}, wrap_builder::{WrapBuilder, WrapResult}, }, @@ -163,3 +163,34 @@ fn test_successful_spl_token_2022_to_token_2022() { assert_wrap_result(starting_amount, wrap_amount, &wrap_result); } + +#[test] +fn test_wrap_with_spl_token_multisig() { + let starting_amount = 500_000; + let wrap_amount = 8_000; + let multisig = setup_multisig(TokenProgram::SplToken); + + let wrap_result = WrapBuilder::default() + .transfer_authority(multisig) + .recipient_starting_amount(starting_amount) + .wrap_amount(wrap_amount) + .execute(); + + assert_wrap_result(starting_amount, wrap_amount, &wrap_result); +} + +#[test] +fn test_wrap_with_token_2022_multisig() { + let starting_amount = 10_000; + let wrap_amount = 252; + let multisig = setup_multisig(TokenProgram::SplToken2022); + + let wrap_result = WrapBuilder::default() + .transfer_authority(multisig) + .unwrapped_token_program(TokenProgram::SplToken2022) + .recipient_starting_amount(starting_amount) + .wrap_amount(wrap_amount) + .execute(); + + assert_wrap_result(starting_amount, wrap_amount, &wrap_result); +}