From bb7a3f48483faa6ccbe32ff2c12a9f80ea91d082 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 31 Jan 2025 18:14:58 -0500 Subject: [PATCH 01/20] Update `accept_token_authority` to account for multisig token authority --- .../src/instructions/admin.rs | 27 +++++++++++++++---- .../example-native-token-transfers/src/lib.rs | 6 +++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index e5c87d7f5..4edf11da0 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -13,6 +13,7 @@ use crate::{ pending_token_authority::PendingTokenAuthority, queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, registered_transceiver::RegisteredTransceiver, + spl_multisig::SplMultisig, }; // * Transfer ownership @@ -159,7 +160,7 @@ pub fn claim_ownership(ctx: Context) -> Result<()> { // * Set token authority #[derive(Accounts)] -pub struct AcceptTokenAuthority<'info> { +pub struct AcceptTokenAuthorityBase<'info> { #[account( has_one = mint, constraint = config.paused @ NTTError::NotPaused, @@ -176,22 +177,38 @@ pub struct AcceptTokenAuthority<'info> { /// CHECK: The constraints enforce this is valid mint authority pub token_authority: UncheckedAccount<'info>, - pub current_authority: Signer<'info>, + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, pub token_program: Interface<'info, token_interface::TokenInterface>, } +#[derive(Accounts)] +pub struct AcceptTokenAuthority<'info> { + pub common: AcceptTokenAuthorityBase<'info>, + + pub current_authority: Signer<'info>, +} + pub fn accept_token_authority(ctx: Context) -> Result<()> { token_interface::set_authority( CpiContext::new( - ctx.accounts.token_program.to_account_info(), + ctx.accounts.common.token_program.to_account_info(), token_interface::SetAuthority { - account_or_mint: ctx.accounts.mint.to_account_info(), + account_or_mint: ctx.accounts.common.mint.to_account_info(), current_authority: ctx.accounts.current_authority.to_account_info(), }, ), AuthorityType::MintTokens, - Some(ctx.accounts.token_authority.key()), + if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority.key()) + } else { + Some(ctx.accounts.common.token_authority.key()) + }, ) } diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 7b480b498..6dbdae0cc 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -146,8 +146,10 @@ pub mod example_native_token_transfers { instructions::accept_token_authority(ctx) } - pub fn set_token_authority(ctx: Context) -> Result<()> { - instructions::set_token_authority(ctx) + pub fn accept_token_authority_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityMultisig<'info>>, + ) -> Result<()> { + instructions::accept_token_authority_multisig(ctx) } pub fn set_token_authority_one_step_unchecked( From e9445532cd8500ac13e24b68e3d342e1258cde5f Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 31 Jan 2025 18:15:36 -0500 Subject: [PATCH 02/20] Add `accept_token_authority_multisig` ix --- .../src/instructions/admin.rs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 4edf11da0..ed54d17a8 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -212,6 +212,49 @@ pub fn accept_token_authority(ctx: Context) -> Result<()> ) } +#[derive(Accounts)] +pub struct AcceptTokenAuthorityMultisig<'info> { + pub common: AcceptTokenAuthorityBase<'info>, + + pub current_multisig_authority: InterfaceAccount<'info, SplMultisig>, +} + +pub fn accept_token_authority_multisig<'info>(ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityMultisig<'info>>) -> Result<()> { + let new_authority = if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { + multisig_token_authority.to_account_info() + } else { + ctx.accounts.common.token_authority.to_account_info() + }; + + let mut signer_pubkeys: Vec<&Pubkey> = Vec::new(); + let mut account_infos = vec![ + ctx.accounts.common.mint.to_account_info(), + new_authority.clone(), + ctx.accounts.current_multisig_authority.to_account_info(), + ]; + + // handle additional signers + { + let num_additional_signers = ctx.accounts.current_multisig_authority.m as usize - 1; + let additional_signers = &ctx.remaining_accounts[..num_additional_signers]; + signer_pubkeys.extend(additional_signers.iter().map(|x| x.key)); + account_infos.extend_from_slice(additional_signers); + } + + solana_program::program::invoke( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &ctx.accounts.current_multisig_authority.key(), + &signer_pubkeys, + )?, + account_infos.as_slice(), + )?; + Ok(()) +} + #[derive(Accounts)] pub struct SetTokenAuthority<'info> { #[account( From f26900190cf188179ca3e9904658b746d88e0f3a Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 31 Jan 2025 18:16:12 -0500 Subject: [PATCH 03/20] Update `set_token_authority_one_step_unchecked` to account for multisig token authority --- .../src/instructions/admin.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index ed54d17a8..b44032320 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -276,10 +276,68 @@ pub struct SetTokenAuthority<'info> { /// CHECK: The constraints enforce this is valid mint authority pub token_authority: UncheckedAccount<'info>, + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, + /// CHECK: This account will be the signer in the [claim_token_authority] instruction. pub new_authority: UncheckedAccount<'info>, } +#[derive(Accounts)] +pub struct SetTokenAuthorityUnchecked<'info> { + pub common: SetTokenAuthority<'info>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, +} + +pub fn set_token_authority_one_step_unchecked( + ctx: Context, +) -> Result<()> { + if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.common.new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], + )?, + &[ + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.new_authority.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info() + ], + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]] + )?; + } else { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.common.new_authority.key()), + )?; + } + Ok(()) +} + #[derive(Accounts)] pub struct SetTokenAuthorityChecked<'info> { #[account( From 960f080db9a5e64f03bac80765c4537eda6fb1ef Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 31 Jan 2025 18:17:10 -0500 Subject: [PATCH 04/20] Update `set_token_authority` to account for multisig token authority --- .../src/instructions/admin.rs | 34 ++++--------------- .../example-native-token-transfers/src/lib.rs | 4 +++ 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index b44032320..0f7cdcf1a 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -341,7 +341,12 @@ pub fn set_token_authority_one_step_unchecked( #[derive(Accounts)] pub struct SetTokenAuthorityChecked<'info> { #[account( - constraint = common.token_authority.key() == common.mint.mint_authority.unwrap() @ NTTError::InvalidMintAuthority + constraint = + (common.multisig_token_authority.is_some() && + common.multisig_token_authority.clone().unwrap().key() == common.mint.mint_authority.unwrap()) || + (common.multisig_token_authority.is_none() && + common.token_authority.key() == common.mint.mint_authority.unwrap()) + @ NTTError::InvalidMintAuthority )] pub common: SetTokenAuthority<'info>, @@ -371,33 +376,6 @@ pub fn set_token_authority(ctx: Context) -> Result<()> Ok(()) } -#[derive(Accounts)] -pub struct SetTokenAuthorityUnchecked<'info> { - pub common: SetTokenAuthority<'info>, - - pub token_program: Interface<'info, token_interface::TokenInterface>, -} - -pub fn set_token_authority_one_step_unchecked( - ctx: Context, -) -> Result<()> { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.common.new_authority.key()), - ) -} - // * Claim token authority #[derive(Accounts)] diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 6dbdae0cc..591770a43 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -158,6 +158,10 @@ pub mod example_native_token_transfers { instructions::set_token_authority_one_step_unchecked(ctx) } + pub fn set_token_authority(ctx: Context) -> Result<()> { + instructions::set_token_authority(ctx) + } + pub fn revert_token_authority(ctx: Context) -> Result<()> { instructions::revert_token_authority(ctx) } From a31365a64c40753bf22f447e66dc0db0d61dc616 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 31 Jan 2025 18:17:52 -0500 Subject: [PATCH 05/20] Update `claim_token_authority`and add `claim_token_authority_to_multisig` to account for multisig token authority --- .../src/instructions/admin.rs | 121 ++++++++++++++++-- .../example-native-token-transfers/src/lib.rs | 6 + 2 files changed, 115 insertions(+), 12 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 0f7cdcf1a..35a6fb0e3 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -396,6 +396,13 @@ pub struct ClaimTokenAuthorityBase<'info> { /// CHECK: The seeds constraint enforces that this is the correct address pub token_authority: UncheckedAccount<'info>, + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, + #[account(mut)] /// CHECK: the `pending_token_authority` constraint enforces that this is the correct address pub rent_payer: UncheckedAccount<'info>, @@ -439,21 +446,111 @@ pub struct ClaimTokenAuthority<'info> { } pub fn claim_token_authority(ctx: Context) -> Result<()> { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, + if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], + )?, + &[ + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.new_authority.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info() + ], &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.new_authority.key()), - ) + ]] + )?; + } else { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.common.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.new_authority.key()), + )?; + } + Ok(()) +} + +#[derive(Accounts)] +pub struct ClaimTokenAuthorityToMultisig<'info> { + pub common: ClaimTokenAuthorityBase<'info>, + + #[account( + address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority + )] + /// CHECK: The remaining accounts are treated as required signers for the multisig to be validated + pub new_multisig_authority: InterfaceAccount<'info, SplMultisig>, +} + +pub fn claim_token_authority_to_multisig(ctx: Context) -> Result<()> { + // SPL Multisig cannot be a Signer so we simulate multisig signing using ctx.remaining_accounts as + // required signers to validate it + { + let multisig = ctx.accounts.new_multisig_authority.to_account_info(); + token_interface::spl_token_2022::processor::Processor::validate_owner( + &ctx.accounts.common.token_program.key(), + &multisig.key(), + &multisig, + multisig.data_len(), + &ctx.remaining_accounts, + )?; + } + + if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.new_multisig_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], + )?, + &[ + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.new_multisig_authority.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info() + ], + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]] + )?; + } else { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.common.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.new_multisig_authority.key()), + )?; + } + Ok(()) } // * Set peers diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 591770a43..0b9ff22a1 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -170,6 +170,12 @@ pub mod example_native_token_transfers { instructions::claim_token_authority(ctx) } + pub fn claim_token_authority_to_multisig( + ctx: Context, + ) -> Result<()> { + instructions::claim_token_authority_to_multisig(ctx) + } + pub fn set_paused(ctx: Context, pause: bool) -> Result<()> { instructions::set_paused(ctx, pause) } From 2fc9fcbff2e2cb7304d17d47add70af3d6676900 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 11:43:04 -0500 Subject: [PATCH 06/20] solana: Rename ix to `acceptTokenAuthorityToMultisig` --- .../src/instructions/admin.rs | 4 ++-- solana/programs/example-native-token-transfers/src/lib.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 35a6fb0e3..c8f1a1d0b 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -213,13 +213,13 @@ pub fn accept_token_authority(ctx: Context) -> Result<()> } #[derive(Accounts)] -pub struct AcceptTokenAuthorityMultisig<'info> { +pub struct AcceptTokenAuthorityFromMultisig<'info> { pub common: AcceptTokenAuthorityBase<'info>, pub current_multisig_authority: InterfaceAccount<'info, SplMultisig>, } -pub fn accept_token_authority_multisig<'info>(ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityMultisig<'info>>) -> Result<()> { +pub fn accept_token_authority_from_multisig<'info>(ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>) -> Result<()> { let new_authority = if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { multisig_token_authority.to_account_info() } else { diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 0b9ff22a1..f3aed6dfb 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -146,10 +146,10 @@ pub mod example_native_token_transfers { instructions::accept_token_authority(ctx) } - pub fn accept_token_authority_multisig<'info>( - ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityMultisig<'info>>, + pub fn accept_token_authority_from_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>, ) -> Result<()> { - instructions::accept_token_authority_multisig(ctx) + instructions::accept_token_authority_from_multisig(ctx) } pub fn set_token_authority_one_step_unchecked( From 69aa74215bd76dc938d11f690437d319bedf317e Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 14:56:46 -0500 Subject: [PATCH 07/20] solana: Pass all remaining_accounts as required signers in `accept_token_authority_from_multisig` --- .../src/instructions/admin.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index c8f1a1d0b..2497e4169 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -232,13 +232,11 @@ pub fn accept_token_authority_from_multisig<'info>(ctx: Context<'_, '_, '_, 'inf new_authority.clone(), ctx.accounts.current_multisig_authority.to_account_info(), ]; - - // handle additional signers + + // pass ctx.remaining_accounts as required signers { - let num_additional_signers = ctx.accounts.current_multisig_authority.m as usize - 1; - let additional_signers = &ctx.remaining_accounts[..num_additional_signers]; - signer_pubkeys.extend(additional_signers.iter().map(|x| x.key)); - account_infos.extend_from_slice(additional_signers); + signer_pubkeys.extend(ctx.remaining_accounts.iter().map(|x| x.key)); + account_infos.extend_from_slice(ctx.remaining_accounts); } solana_program::program::invoke( From f102b3afd7b5c4bb77455d9a4dff7f4394b5c03d Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 14:57:06 -0500 Subject: [PATCH 08/20] solana: Fix lint --- .../src/instructions/admin.rs | 102 +++++++++--------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 2497e4169..f0806d1b0 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -178,7 +178,7 @@ pub struct AcceptTokenAuthorityBase<'info> { pub token_authority: UncheckedAccount<'info>, #[account( - constraint = multisig_token_authority.m == 1 + constraint = multisig_token_authority.m == 1 && multisig_token_authority.signers.contains(&token_authority.key()) @ NTTError::InvalidMultisig, )] @@ -207,8 +207,8 @@ pub fn accept_token_authority(ctx: Context) -> Result<()> if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { Some(multisig_token_authority.key()) } else { - Some(ctx.accounts.common.token_authority.key()) - }, + Some(ctx.accounts.common.token_authority.key()) + }, ) } @@ -216,16 +216,20 @@ pub fn accept_token_authority(ctx: Context) -> Result<()> pub struct AcceptTokenAuthorityFromMultisig<'info> { pub common: AcceptTokenAuthorityBase<'info>, + /// CHECK: The remaining accounts are treated as required signers for the multisig pub current_multisig_authority: InterfaceAccount<'info, SplMultisig>, } -pub fn accept_token_authority_from_multisig<'info>(ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>) -> Result<()> { - let new_authority = if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { - multisig_token_authority.to_account_info() - } else { - ctx.accounts.common.token_authority.to_account_info() - }; - +pub fn accept_token_authority_from_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>, +) -> Result<()> { + let new_authority = + if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { + multisig_token_authority.to_account_info() + } else { + ctx.accounts.common.token_authority.to_account_info() + }; + let mut signer_pubkeys: Vec<&Pubkey> = Vec::new(); let mut account_infos = vec![ ctx.accounts.common.mint.to_account_info(), @@ -238,7 +242,7 @@ pub fn accept_token_authority_from_multisig<'info>(ctx: Context<'_, '_, '_, 'inf signer_pubkeys.extend(ctx.remaining_accounts.iter().map(|x| x.key)); account_infos.extend_from_slice(ctx.remaining_accounts); } - + solana_program::program::invoke( &spl_token_2022::instruction::set_authority( &ctx.accounts.common.token_program.key(), @@ -275,7 +279,7 @@ pub struct SetTokenAuthority<'info> { pub token_authority: UncheckedAccount<'info>, #[account( - constraint = multisig_token_authority.m == 1 + constraint = multisig_token_authority.m == 1 && multisig_token_authority.signers.contains(&token_authority.key()) @ NTTError::InvalidMultisig, )] @@ -297,24 +301,24 @@ pub fn set_token_authority_one_step_unchecked( ) -> Result<()> { if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.common.new_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], + &spl_token_2022::instruction::set_authority( + &ctx.accounts.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.common.new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], )?, &[ ctx.accounts.common.mint.to_account_info(), ctx.accounts.common.new_authority.to_account_info(), multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info() + ctx.accounts.common.token_authority.to_account_info(), ], &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], - ]] + ]], )?; } else { token_interface::set_authority( @@ -339,12 +343,12 @@ pub fn set_token_authority_one_step_unchecked( #[derive(Accounts)] pub struct SetTokenAuthorityChecked<'info> { #[account( - constraint = - (common.multisig_token_authority.is_some() && - common.multisig_token_authority.clone().unwrap().key() == common.mint.mint_authority.unwrap()) || - (common.multisig_token_authority.is_none() && - common.token_authority.key() == common.mint.mint_authority.unwrap()) - @ NTTError::InvalidMintAuthority + constraint = + (common.multisig_token_authority.is_some() && + common.multisig_token_authority.clone().unwrap().key() == common.mint.mint_authority.unwrap()) || + (common.multisig_token_authority.is_none() && + common.token_authority.key() == common.mint.mint_authority.unwrap()) + @ NTTError::InvalidMintAuthority )] pub common: SetTokenAuthority<'info>, @@ -395,7 +399,7 @@ pub struct ClaimTokenAuthorityBase<'info> { pub token_authority: UncheckedAccount<'info>, #[account( - constraint = multisig_token_authority.m == 1 + constraint = multisig_token_authority.m == 1 && multisig_token_authority.signers.contains(&token_authority.key()) @ NTTError::InvalidMultisig, )] @@ -446,24 +450,24 @@ pub struct ClaimTokenAuthority<'info> { pub fn claim_token_authority(ctx: Context) -> Result<()> { if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.common.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.new_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], )?, &[ ctx.accounts.common.mint.to_account_info(), ctx.accounts.new_authority.to_account_info(), multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info() + ctx.accounts.common.token_authority.to_account_info(), ], &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], - ]] + ]], )?; } else { token_interface::set_authority( @@ -496,10 +500,12 @@ pub struct ClaimTokenAuthorityToMultisig<'info> { pub new_multisig_authority: InterfaceAccount<'info, SplMultisig>, } -pub fn claim_token_authority_to_multisig(ctx: Context) -> Result<()> { +pub fn claim_token_authority_to_multisig( + ctx: Context, +) -> Result<()> { // SPL Multisig cannot be a Signer so we simulate multisig signing using ctx.remaining_accounts as // required signers to validate it - { + { let multisig = ctx.accounts.new_multisig_authority.to_account_info(); token_interface::spl_token_2022::processor::Processor::validate_owner( &ctx.accounts.common.token_program.key(), @@ -512,24 +518,24 @@ pub fn claim_token_authority_to_multisig(ctx: Context Date: Mon, 3 Feb 2025 14:58:03 -0500 Subject: [PATCH 09/20] solana: Add TS helper functions --- solana/ts/lib/ntt.ts | 142 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 4 deletions(-) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 4aed49162..105c4d710 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -849,6 +849,7 @@ export namespace NTT { config: NttBindings.Config, args: { currentAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { @@ -856,15 +857,78 @@ export namespace NTT { return program.methods .acceptTokenAuthority() .accountsStrict({ - config: pdas.configAccount(), - mint: config.mint, - tokenProgram: config.tokenProgram, - tokenAuthority: pdas.tokenAuthority(), + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenProgram: config.tokenProgram, + tokenAuthority: pdas.tokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, currentAuthority: args.currentAuthority, }) .instruction(); } + export async function createAcceptTokenAuthorityFromMultisigInstruction( + program: Program>, + config: NttBindings.Config, + args: { + currentMultisigAuthority: PublicKey; + additionalSigners: PublicKey[]; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .acceptTokenAuthorityFromMultisig() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenProgram: config.tokenProgram, + tokenAuthority: pdas.tokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + currentMultisigAuthority: args.currentMultisigAuthority, + }) + .remainingAccounts( + args.additionalSigners.map((pubkey) => ({ + pubkey, + isSigner: true, + isWritable: false, + })) + ) + .instruction(); + } + + export async function createSetTokenAuthorityOneStepUncheckedInstruction( + program: Program>, + config: NttBindings.Config, + args: { + owner: PublicKey; + newAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .setTokenAuthorityOneStepUnchecked() + .accountsStrict({ + common: { + config: pdas.configAccount(), + tokenAuthority: pdas.tokenAuthority(), + mint: config.mint, + owner: args.owner, + newAuthority: args.newAuthority, + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + tokenProgram: config.tokenProgram, + }) + .instruction(); + } + export async function createSetTokenAuthorityInstruction( program: Program>, config: NttBindings.Config, @@ -872,6 +936,7 @@ export namespace NTT { rentPayer: PublicKey; owner: PublicKey; newAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { @@ -885,6 +950,7 @@ export namespace NTT { mint: config.mint, owner: args.owner, newAuthority: args.newAuthority, + multisigTokenAuthority: args.multisigTokenAuthority ?? null, }, rentPayer: args.rentPayer, pendingTokenAuthority: pdas.pendingTokenAuthority(), @@ -899,6 +965,7 @@ export namespace NTT { args: { rentPayer: PublicKey; owner: PublicKey; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { @@ -914,12 +981,79 @@ export namespace NTT { systemProgram: SystemProgram.programId, rentPayer: args.rentPayer, pendingTokenAuthority: pdas.pendingTokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, }, owner: args.owner, }) .instruction(); } + export async function createClaimTokenAuthorityInstruction( + program: Program>, + config: NttBindings.Config, + args: { + rentPayer: PublicKey; + newAuthority: PublicKey; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .claimTokenAuthority() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenAuthority: pdas.tokenAuthority(), + tokenProgram: config.tokenProgram, + systemProgram: SystemProgram.programId, + rentPayer: args.rentPayer, + pendingTokenAuthority: pdas.pendingTokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + newAuthority: args.newAuthority, + }) + .instruction(); + } + + export async function createClaimTokenAuthorityToMultisigInstruction( + program: Program>, + config: NttBindings.Config, + args: { + rentPayer: PublicKey; + newMultisigAuthority: PublicKey; + additionalSigners: PublicKey[]; + multisigTokenAuthority?: PublicKey; + }, + pdas?: Pdas + ) { + pdas = pdas ?? NTT.pdas(program.programId); + return program.methods + .claimTokenAuthorityToMultisig() + .accountsStrict({ + common: { + config: pdas.configAccount(), + mint: config.mint, + tokenAuthority: pdas.tokenAuthority(), + tokenProgram: config.tokenProgram, + systemProgram: SystemProgram.programId, + rentPayer: args.rentPayer, + pendingTokenAuthority: pdas.pendingTokenAuthority(), + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }, + newMultisigAuthority: args.newMultisigAuthority, + }) + .remainingAccounts( + args.additionalSigners.map((pubkey) => ({ + pubkey, + isSigner: true, + isWritable: false, + })) + ) + .instruction(); + } + export async function createSetPeerInstruction( program: Program>, args: { From ad25ae1eb76ed125df3208920adb1bc2f1099d9e Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 14:58:15 -0500 Subject: [PATCH 10/20] solana: Update IDL --- .../json/example_native_token_transfers.json | 182 +++++++-- .../ts/example_native_token_transfers.ts | 364 +++++++++++++++--- 2 files changed, 474 insertions(+), 72 deletions(-) diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index ea99bdc9e..457cfd5f8 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -892,24 +892,124 @@ "name": "acceptTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "currentAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "acceptTokenAuthorityFromMultisig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "tokenAuthority", + "name": "currentMultisigAuthority", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ { - "name": "currentAuthority", - "isMut": false, - "isSigner": true + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] }, { "name": "tokenProgram", @@ -945,6 +1045,12 @@ "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "newAuthority", "isMut": false, @@ -971,7 +1077,7 @@ "args": [] }, { - "name": "setTokenAuthorityOneStepUnchecked", + "name": "revertTokenAuthority", "accounts": [ { "name": "common", @@ -982,37 +1088,53 @@ "isSigner": false }, { - "name": "owner", + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "mint", + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "rentPayer", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } ] }, { - "name": "tokenProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] }, { - "name": "revertTokenAuthority", + "name": "claimTokenAuthority", "accounts": [ { "name": "common", @@ -1032,6 +1154,12 @@ "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1055,7 +1183,7 @@ ] }, { - "name": "owner", + "name": "newAuthority", "isMut": false, "isSigner": true } @@ -1063,7 +1191,7 @@ "args": [] }, { - "name": "claimTokenAuthority", + "name": "claimTokenAuthorityToMultisig", "accounts": [ { "name": "common", @@ -1083,6 +1211,12 @@ "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1106,9 +1240,9 @@ ] }, { - "name": "newAuthority", + "name": "newMultisigAuthority", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index 30ccc2364..e0305fc6b 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -892,24 +892,124 @@ export type ExampleNativeTokenTransfers = { "name": "acceptTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "currentAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "acceptTokenAuthorityFromMultisig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "tokenAuthority", + "name": "currentMultisigAuthority", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ { - "name": "currentAuthority", - "isMut": false, - "isSigner": true + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] }, { "name": "tokenProgram", @@ -945,6 +1045,12 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "newAuthority", "isMut": false, @@ -971,7 +1077,7 @@ export type ExampleNativeTokenTransfers = { "args": [] }, { - "name": "setTokenAuthorityOneStepUnchecked", + "name": "revertTokenAuthority", "accounts": [ { "name": "common", @@ -982,37 +1088,53 @@ export type ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "owner", + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "mint", + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "rentPayer", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } ] }, { - "name": "tokenProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] }, { - "name": "revertTokenAuthority", + "name": "claimTokenAuthority", "accounts": [ { "name": "common", @@ -1032,6 +1154,12 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1055,7 +1183,7 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "owner", + "name": "newAuthority", "isMut": false, "isSigner": true } @@ -1063,7 +1191,7 @@ export type ExampleNativeTokenTransfers = { "args": [] }, { - "name": "claimTokenAuthority", + "name": "claimTokenAuthorityToMultisig", "accounts": [ { "name": "common", @@ -1083,6 +1211,12 @@ export type ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -1106,9 +1240,9 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "newAuthority", + "name": "newMultisigAuthority", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] @@ -3350,24 +3484,124 @@ export const IDL: ExampleNativeTokenTransfers = { "name": "acceptTokenAuthority", "accounts": [ { - "name": "config", - "isMut": false, - "isSigner": false + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "currentAuthority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "acceptTokenAuthorityFromMultisig", + "accounts": [ + { + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "tokenAuthority", + "name": "currentMultisigAuthority", "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setTokenAuthorityOneStepUnchecked", + "accounts": [ { - "name": "currentAuthority", - "isMut": false, - "isSigner": true + "name": "common", + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "newAuthority", + "isMut": false, + "isSigner": false + } + ] }, { "name": "tokenProgram", @@ -3403,6 +3637,12 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "newAuthority", "isMut": false, @@ -3429,7 +3669,7 @@ export const IDL: ExampleNativeTokenTransfers = { "args": [] }, { - "name": "setTokenAuthorityOneStepUnchecked", + "name": "revertTokenAuthority", "accounts": [ { "name": "common", @@ -3440,37 +3680,53 @@ export const IDL: ExampleNativeTokenTransfers = { "isSigner": false }, { - "name": "owner", + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenAuthority", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "mint", + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "rentPayer", "isMut": true, "isSigner": false }, { - "name": "tokenAuthority", + "name": "pendingTokenAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", "isMut": false, "isSigner": false }, { - "name": "newAuthority", + "name": "systemProgram", "isMut": false, "isSigner": false } ] }, { - "name": "tokenProgram", + "name": "owner", "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [] }, { - "name": "revertTokenAuthority", + "name": "claimTokenAuthority", "accounts": [ { "name": "common", @@ -3490,6 +3746,12 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -3513,7 +3775,7 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "owner", + "name": "newAuthority", "isMut": false, "isSigner": true } @@ -3521,7 +3783,7 @@ export const IDL: ExampleNativeTokenTransfers = { "args": [] }, { - "name": "claimTokenAuthority", + "name": "claimTokenAuthorityToMultisig", "accounts": [ { "name": "common", @@ -3541,6 +3803,12 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": false, "isSigner": false }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "rentPayer", "isMut": true, @@ -3564,9 +3832,9 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "newAuthority", + "name": "newMultisigAuthority", "isMut": false, - "isSigner": true + "isSigner": false } ], "args": [] From fbda72a2337cfac920eb0e3ebf88c734ce57574b Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 14:59:31 -0500 Subject: [PATCH 11/20] solana: Add token authority transfer test cases --- solana/tests/anchor.test.ts | 332 +++++++++++++++++++++++++++++++++++- 1 file changed, 327 insertions(+), 5 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index a582890b5..da1b70238 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -130,6 +130,7 @@ describe("example-native-token-transfers", () => { let sender: AccountAddress<"Solana">; let multisig: anchor.web3.PublicKey; let tokenAddress: string; + let multisigTokenAuthority: anchor.web3.PublicKey; beforeAll(async () => { try { @@ -226,7 +227,7 @@ describe("example-native-token-transfers", () => { describe("Burning", () => { beforeAll(async () => { try { - multisig = await spl.createMultisig( + multisigTokenAuthority = await spl.createMultisig( connection, payer, [owner.publicKey, ntt.pdas.tokenAuthority()], @@ -241,7 +242,7 @@ describe("example-native-token-transfers", () => { mint.publicKey, owner, spl.AuthorityType.MintTokens, - multisig, + multisigTokenAuthority, [], undefined, TOKEN_PROGRAM @@ -252,7 +253,7 @@ describe("example-native-token-transfers", () => { mint: mint.publicKey, outboundLimit: 1000000n, mode: "burning", - multisig, + multisig: multisigTokenAuthority, }); await signSendWait(ctx, initTxs, signer); @@ -357,6 +358,327 @@ describe("example-native-token-transfers", () => { expect(balance.value.amount).toBe("9900000"); }); + describe("Can transfer mint authority to-and-from NTT manager", () => { + const newAuthority = anchor.web3.Keypair.generate(); + let newMultisigAuthority: anchor.web3.PublicKey; + + beforeAll(async () => { + newMultisigAuthority = await spl.createMultisig( + connection, + payer, + [owner.publicKey, newAuthority.publicKey], + 2, + anchor.web3.Keypair.generate(), + undefined, + TOKEN_PROGRAM + ); + }); + + it("Fails when contract is not paused", async () => { + try { + const transaction = new anchor.web3.Transaction().add( + await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( + ntt.program, + await ntt.getConfig(), + { + owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ) + ); + transaction.feePayer = payer.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + payer, + ]); + // tx should fail so this expect should never be hit + expect(false).toBeTruthy(); + } catch (e) { + expect(e).toBeInstanceOf(anchor.web3.SendTransactionError); + const parsedError = anchor.AnchorError.parse( + (e as anchor.web3.SendTransactionError).logs ?? [] + ); + expect(parsedError?.error.errorCode).toEqual({ + code: "NotPaused", + number: 6024, + }); + } finally { + const mintInfo = await spl.getMint( + connection, + mint.publicKey, + undefined, + TOKEN_PROGRAM + ); + expect(mintInfo.mintAuthority).toEqual(multisigTokenAuthority); + } + }); + + test("Multisig(owner, TA) -> newAuthority", async () => { + // retry after pausing contract + const pauseTxs = await ntt.pause(new SolanaAddress(payer.publicKey)); + await signSendWait(ctx, pauseTxs, signer); + + const transaction = new anchor.web3.Transaction().add( + await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( + ntt.program, + await ntt.getConfig(), + { + owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ) + ); + transaction.feePayer = payer.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + payer, + ]); + + const mintInfo = await spl.getMint( + connection, + mint.publicKey, + undefined, + TOKEN_PROGRAM + ); + expect(mintInfo.mintAuthority).toEqual(newAuthority.publicKey); + }); + + test("newAuthority -> TA", async () => { + const transaction = new anchor.web3.Transaction().add( + await NTT.createAcceptTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + currentAuthority: newAuthority.publicKey, + } + ) + ); + transaction.feePayer = payer.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + payer, + newAuthority, + ]); + + const mintInfo = await spl.getMint( + connection, + mint.publicKey, + undefined, + TOKEN_PROGRAM + ); + expect(mintInfo.mintAuthority).toEqual(ntt.pdas.tokenAuthority()); + }); + + test("TA -> Multisig(owner, newAuthority)", async () => { + // set token authority: TA -> newMultisigAuthority + const setTransaction = new anchor.web3.Transaction().add( + await NTT.createSetTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: new SolanaAddress(await ntt.getOwner()).unwrap(), + owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + newAuthority: newMultisigAuthority, + } + ) + ); + setTransaction.feePayer = payer.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + setTransaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction( + connection, + setTransaction, + [payer] + ); + + // claim token authority: newMultisigAuthority <- TA + const claimTransaction = new anchor.web3.Transaction().add( + await NTT.createClaimTokenAuthorityToMultisigInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: new SolanaAddress(await ntt.getOwner()).unwrap(), + newMultisigAuthority, + additionalSigners: [newAuthority.publicKey, owner.publicKey], + } + ) + ); + claimTransaction.feePayer = payer.publicKey; + claimTransaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction( + connection, + claimTransaction, + [payer, newAuthority, owner] + ); + + const mintInfo = await spl.getMint( + connection, + mint.publicKey, + undefined, + TOKEN_PROGRAM + ); + expect(mintInfo.mintAuthority).toEqual(newMultisigAuthority); + }); + + test("Multisig(owner, newAuthority) -> Multisig(owner, TA)", async () => { + const transaction = new anchor.web3.Transaction().add( + await NTT.createAcceptTokenAuthorityFromMultisigInstruction( + ntt.program, + await ntt.getConfig(), + { + currentMultisigAuthority: newMultisigAuthority, + additionalSigners: [newAuthority.publicKey, owner.publicKey], + multisigTokenAuthority, + } + ) + ); + transaction.feePayer = payer.publicKey; + const { blockhash } = await connection.getLatestBlockhash(); + transaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + payer, + newAuthority, + owner, + ]); + + const mintInfo = await spl.getMint( + connection, + mint.publicKey, + undefined, + TOKEN_PROGRAM + ); + expect(mintInfo.mintAuthority).toEqual(multisigTokenAuthority); + }); + + it("Fails on claim after revert", async () => { + try { + // fund newAuthority for it to be rent payer + const signature = await connection.requestAirdrop( + newAuthority.publicKey, + anchor.web3.LAMPORTS_PER_SOL + ); + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(); + await connection.confirmTransaction({ + blockhash, + lastValidBlockHeight, + signature, + }); + let newAuthorityBalance = ( + await connection.getAccountInfo(newAuthority.publicKey) + )?.lamports; + expect(newAuthorityBalance).toBe(anchor.web3.LAMPORTS_PER_SOL); + + // set token authority: multisigTokenAuthority -> newAuthority + const setTransaction = new anchor.web3.Transaction().add( + await NTT.createSetTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ) + ); + setTransaction.feePayer = payer.publicKey; + setTransaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction( + connection, + setTransaction, + [payer, newAuthority] + ); + newAuthorityBalance = ( + await connection.getAccountInfo(newAuthority.publicKey) + )?.lamports; + const pendingTokenAuthorityRentExemptAmount = + await connection.getMinimumBalanceForRentExemption( + ntt.program.account.pendingTokenAuthority.size + ); + expect(newAuthorityBalance).toBe( + anchor.web3.LAMPORTS_PER_SOL - pendingTokenAuthorityRentExemptAmount + ); + + // revert token authority: multisigTokenAuthority + const revertTransaction = new anchor.web3.Transaction().add( + await NTT.createRevertTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + multisigTokenAuthority, + } + ) + ); + revertTransaction.feePayer = payer.publicKey; + revertTransaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction( + connection, + revertTransaction, + [payer] + ); + newAuthorityBalance = ( + await connection.getAccountInfo(newAuthority.publicKey) + )?.lamports; + expect(newAuthorityBalance).toBe(anchor.web3.LAMPORTS_PER_SOL); + + // claim token authority: newAuthority <- multisigTokenAuthority + const claimTransaction = new anchor.web3.Transaction().add( + await NTT.createClaimTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ) + ); + claimTransaction.feePayer = payer.publicKey; + claimTransaction.recentBlockhash = blockhash; + await anchor.web3.sendAndConfirmTransaction( + connection, + claimTransaction, + [payer, newAuthority] + ); + // tx should fail so this expect should never be hit + expect(false).toBeTruthy(); + } catch (e) { + expect(e).toBeInstanceOf(anchor.web3.SendTransactionError); + const parsedError = anchor.AnchorError.parse( + (e as anchor.web3.SendTransactionError).logs ?? [] + ); + expect(parsedError?.error.errorCode).toEqual({ + code: "AccountNotInitialized", + number: 3012, + }); + } finally { + const mintInfo = await spl.getMint( + connection, + mint.publicKey, + undefined, + TOKEN_PROGRAM + ); + expect(mintInfo.mintAuthority).toEqual(multisigTokenAuthority); + } + }); + + afterAll(async () => { + // unpause + const unpauseTxs = await ntt.unpause( + new SolanaAddress(payer.publicKey) + ); + await signSendWait(ctx, unpauseTxs, signer); + }); + }); + it("Can receive tokens", async () => { const emitter = new testing.mocks.MockEmitter( remoteXcvr.address as UniversalAddress, @@ -396,7 +718,7 @@ describe("example-native-token-transfers", () => { const published = emitter.publishMessage(0, serialized, 200); const rawVaa = guardians.addSignatures(published, [0]); const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); - const redeemTxs = ntt.redeem([vaa], sender, multisig); + const redeemTxs = ntt.redeem([vaa], sender, multisigTokenAuthority); try { await signSendWait(ctx, redeemTxs, signer); } catch (e) { @@ -423,7 +745,7 @@ describe("example-native-token-transfers", () => { payer, mint.publicKey, dest.address, - multisig, + multisigTokenAuthority, 1, [owner], undefined, From 3f20aa94c88ebad24335b2e628d2821373d84f72 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 15:00:27 -0500 Subject: [PATCH 12/20] solana: Make comment/function syntax consistent --- solana/tests/anchor.test.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index da1b70238..22b7cb793 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -85,7 +85,7 @@ const connection = new anchor.web3.Connection( "confirmed" ); -// Make sure we're using the exact same Connection obj for rpc +// make sure we're using the exact same Connection obj for rpc const ctx: ChainContext<"Devnet", "Solana"> = w .getPlatform("Solana") .getChain("Solana", connection); @@ -128,7 +128,6 @@ describe("example-native-token-transfers", () => { let ntt: SolanaNtt<"Devnet", "Solana">; let signer: Signer; let sender: AccountAddress<"Solana">; - let multisig: anchor.web3.PublicKey; let tokenAddress: string; let multisigTokenAuthority: anchor.web3.PublicKey; @@ -201,7 +200,7 @@ describe("example-native-token-transfers", () => { ); tokenAddress = mint.publicKey.toBase58(); - // Create our contract client + // create our contract client ntt = new SolanaNtt( "Devnet", "Solana", @@ -264,14 +263,14 @@ describe("example-native-token-transfers", () => { }); await signSendWait(ctx, registerTxs, signer); - // Set Wormhole xcvr peer + // set Wormhole xcvr peer const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer( remoteXcvr, sender ); await signSendWait(ctx, setXcvrPeerTxs, signer); - // Set manager peer + // set manager peer const setPeerTxs = ntt.setPeer(remoteMgr, 18, 1000000n, sender); await signSendWait(ctx, setPeerTxs, signer); } catch (e) { @@ -308,7 +307,7 @@ describe("example-native-token-transfers", () => { ]); }); - test("Can send tokens", async () => { + it("Can send tokens", async () => { const amount = 100000n; const sender = Wormhole.parseAddress("Solana", signer.address()); @@ -769,8 +768,8 @@ describe("example-native-token-transfers", () => { }, }; - describe("ABI Versions Test", function () { - test("It initializes from Rpc", async function () { + describe("ABI Versions Test", () => { + test("It initializes from Rpc", async () => { const ntt = await SolanaNtt.fromRpc(connection, { Solana: { ...ctx.config, @@ -783,7 +782,7 @@ describe("example-native-token-transfers", () => { expect(ntt).toBeTruthy(); }); - test("It initializes from constructor", async function () { + test("It initializes from constructor", async () => { const ntt = new SolanaNtt("Devnet", "Solana", connection, { ...ctx.config.contracts, ...{ ntt: overrides["Solana"] }, @@ -791,7 +790,7 @@ describe("example-native-token-transfers", () => { expect(ntt).toBeTruthy(); }); - test("It gets the correct version", async function () { + test("It gets the correct version", async () => { const version = await SolanaNtt.getVersion( connection, { ntt: overrides["Solana"] }, @@ -800,7 +799,7 @@ describe("example-native-token-transfers", () => { expect(version).toBe("3.0.0"); }); - test("It initializes using `emitterAccount` as transceiver address", async function () { + test("It initializes using `emitterAccount` as transceiver address", async () => { const overrideEmitter: (typeof overrides)["Solana"] = JSON.parse( JSON.stringify(overrides["Solana"]) ); @@ -815,7 +814,7 @@ describe("example-native-token-transfers", () => { expect(ntt).toBeTruthy(); }); - test("It gets the correct transceiver type", async function () { + test("It gets the correct transceiver type", async () => { const ntt = new SolanaNtt("Devnet", "Solana", connection, { ...ctx.config.contracts, ...{ ntt: overrides["Solana"] }, From 6e5d603ae7350bcd7907fe2b6ea215e25d1eee8d Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 3 Feb 2025 15:52:51 -0500 Subject: [PATCH 13/20] solana: Fix unneeded borrow --- .../example-native-token-transfers/src/instructions/admin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index f0806d1b0..0e3df5c52 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -512,7 +512,7 @@ pub fn claim_token_authority_to_multisig( &multisig.key(), &multisig, multisig.data_len(), - &ctx.remaining_accounts, + ctx.remaining_accounts, )?; } From 793266738a4825c13040a76e505dc77c5f9af47f Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Feb 2025 18:00:30 -0500 Subject: [PATCH 14/20] solana: Replace if let with match syntax --- .../src/instructions/admin.rs | 232 +++++++++--------- 1 file changed, 119 insertions(+), 113 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 0e3df5c52..0cb24a5f9 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -204,11 +204,10 @@ pub fn accept_token_authority(ctx: Context) -> Result<()> }, ), AuthorityType::MintTokens, - if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { - Some(multisig_token_authority.key()) - } else { - Some(ctx.accounts.common.token_authority.key()) - }, + Some(match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => multisig_token_authority.key(), + None => ctx.accounts.common.token_authority.key(), + }), ) } @@ -223,12 +222,10 @@ pub struct AcceptTokenAuthorityFromMultisig<'info> { pub fn accept_token_authority_from_multisig<'info>( ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>, ) -> Result<()> { - let new_authority = - if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { - multisig_token_authority.to_account_info() - } else { - ctx.accounts.common.token_authority.to_account_info() - }; + let new_authority = match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => multisig_token_authority.to_account_info(), + None => ctx.accounts.common.token_authority.to_account_info(), + }; let mut signer_pubkeys: Vec<&Pubkey> = Vec::new(); let mut account_infos = vec![ @@ -299,44 +296,47 @@ pub struct SetTokenAuthorityUnchecked<'info> { pub fn set_token_authority_one_step_unchecked( ctx: Context, ) -> Result<()> { - if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { - solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.common.new_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], - )?, - &[ - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.common.new_authority.to_account_info(), - multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - } else { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, + match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.common.new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], + )?, + &[ + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.new_authority.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ], &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.common.new_authority.key()), - )?; - } + )?; + } + None => { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.common.new_authority.key()), + )?; + } + }; Ok(()) } @@ -448,44 +448,47 @@ pub struct ClaimTokenAuthority<'info> { } pub fn claim_token_authority(ctx: Context) -> Result<()> { - if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { - solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.common.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.new_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], - )?, - &[ - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.new_authority.to_account_info(), - multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - } else { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, + match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.new_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], + )?, + &[ + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.new_authority.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ], &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.new_authority.key()), - )?; - } + )?; + } + None => { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.common.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.new_authority.key()), + )?; + } + }; Ok(()) } @@ -516,44 +519,47 @@ pub fn claim_token_authority_to_multisig( )?; } - if let Some(multisig_token_authority) = &ctx.accounts.common.multisig_token_authority { - solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.common.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.new_multisig_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], - )?, - &[ - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.new_multisig_authority.to_account_info(), - multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - } else { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, + match &ctx.accounts.common.multisig_token_authority { + Some(multisig_token_authority) => { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + Some(&ctx.accounts.new_multisig_authority.key()), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&ctx.accounts.common.token_authority.key()], + )?, + &[ + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.new_multisig_authority.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ], &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.new_multisig_authority.key()), - )?; - } + )?; + } + None => { + token_interface::set_authority( + CpiContext::new_with_signer( + ctx.accounts.common.token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: ctx.accounts.common.mint.to_account_info(), + current_authority: ctx.accounts.common.token_authority.to_account_info(), + }, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + ), + AuthorityType::MintTokens, + Some(ctx.accounts.new_multisig_authority.key()), + )?; + } + }; Ok(()) } From 08c750f01fdf9146fd611f5f2ac11402716b4f16 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Feb 2025 18:00:51 -0500 Subject: [PATCH 15/20] solana: Simplify constraint using map_or --- .../src/instructions/admin.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 0cb24a5f9..044722f9c 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -344,10 +344,10 @@ pub fn set_token_authority_one_step_unchecked( pub struct SetTokenAuthorityChecked<'info> { #[account( constraint = - (common.multisig_token_authority.is_some() && - common.multisig_token_authority.clone().unwrap().key() == common.mint.mint_authority.unwrap()) || - (common.multisig_token_authority.is_none() && - common.token_authority.key() == common.mint.mint_authority.unwrap()) + common.mint.mint_authority.unwrap() == common.multisig_token_authority.as_ref().map_or( + common.token_authority.key(), + |multisig_token_authority| multisig_token_authority.key() + ) @ NTTError::InvalidMintAuthority )] pub common: SetTokenAuthority<'info>, From 5e7b92cb835010ed930946c59d2d6b179fa161ba Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Feb 2025 18:01:40 -0500 Subject: [PATCH 16/20] solana: Add comment on lack of custom error thrown --- .../example-native-token-transfers/src/instructions/admin.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 044722f9c..0d0c7fb62 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -428,6 +428,7 @@ pub struct RevertTokenAuthority<'info> { pub common: ClaimTokenAuthorityBase<'info>, #[account( + // there is no custom error thrown as this is usually checked via `has_one` on the config address = common.config.owner )] pub owner: Signer<'info>, From a33f8dfd693f98d3d95158697782e113acd4dc91 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Feb 2025 18:48:10 -0500 Subject: [PATCH 17/20] solana: Refactor out `claim_from_(multisig_)token_authority` fn's --- .../src/instructions/admin.rs | 214 ++++++++---------- 1 file changed, 92 insertions(+), 122 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs index 0d0c7fb62..82271bef9 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin.rs @@ -297,47 +297,22 @@ pub fn set_token_authority_one_step_unchecked( ctx: Context, ) -> Result<()> { match &ctx.accounts.common.multisig_token_authority { - Some(multisig_token_authority) => { - solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.common.new_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], - )?, - &[ - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.common.new_authority.to_account_info(), - multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - } - None => { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.common.new_authority.key()), - )?; - } - }; - Ok(()) + Some(multisig_token_authority) => claim_from_multisig_token_authority( + ctx.accounts.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.common.new_authority.key(), + ), + None => claim_from_token_authority( + ctx.accounts.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.common.new_authority.key(), + ), + } } #[derive(Accounts)] @@ -450,47 +425,22 @@ pub struct ClaimTokenAuthority<'info> { pub fn claim_token_authority(ctx: Context) -> Result<()> { match &ctx.accounts.common.multisig_token_authority { - Some(multisig_token_authority) => { - solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.common.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.new_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], - )?, - &[ - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.new_authority.to_account_info(), - multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - } - None => { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.new_authority.key()), - )?; - } - }; - Ok(()) + Some(multisig_token_authority) => claim_from_multisig_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_authority.key(), + ), + None => claim_from_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_authority.key(), + ), + } } #[derive(Accounts)] @@ -521,46 +471,66 @@ pub fn claim_token_authority_to_multisig( } match &ctx.accounts.common.multisig_token_authority { - Some(multisig_token_authority) => { - solana_program::program::invoke_signed( - &spl_token_2022::instruction::set_authority( - &ctx.accounts.common.token_program.key(), - &ctx.accounts.common.mint.key(), - Some(&ctx.accounts.new_multisig_authority.key()), - spl_token_2022::instruction::AuthorityType::MintTokens, - &multisig_token_authority.key(), - &[&ctx.accounts.common.token_authority.key()], - )?, - &[ - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.new_multisig_authority.to_account_info(), - multisig_token_authority.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - } - None => { - token_interface::set_authority( - CpiContext::new_with_signer( - ctx.accounts.common.token_program.to_account_info(), - token_interface::SetAuthority { - account_or_mint: ctx.accounts.common.mint.to_account_info(), - current_authority: ctx.accounts.common.token_authority.to_account_info(), - }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - ), - AuthorityType::MintTokens, - Some(ctx.accounts.new_multisig_authority.key()), - )?; - } - }; + Some(multisig_token_authority) => claim_from_multisig_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_multisig_authority.key(), + ), + None => claim_from_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.bumps.common.token_authority, + ctx.accounts.new_multisig_authority.key(), + ), + } +} + +fn claim_from_token_authority<'info>( + token_program: AccountInfo<'info>, + mint: AccountInfo<'info>, + token_authority: AccountInfo<'info>, + token_authority_bump: u8, + new_authority: Pubkey, +) -> Result<()> { + token_interface::set_authority( + CpiContext::new_with_signer( + token_program.to_account_info(), + token_interface::SetAuthority { + account_or_mint: mint.to_account_info(), + current_authority: token_authority.to_account_info(), + }, + &[&[crate::TOKEN_AUTHORITY_SEED, &[token_authority_bump]]], + ), + AuthorityType::MintTokens, + Some(new_authority), + )?; + Ok(()) +} + +fn claim_from_multisig_token_authority<'info>( + token_program: AccountInfo<'info>, + mint: AccountInfo<'info>, + multisig_token_authority: AccountInfo<'info>, + token_authority: AccountInfo<'info>, + token_authority_bump: u8, + new_authority: Pubkey, +) -> Result<()> { + solana_program::program::invoke_signed( + &spl_token_2022::instruction::set_authority( + &token_program.key(), + &mint.key(), + Some(&new_authority), + spl_token_2022::instruction::AuthorityType::MintTokens, + &multisig_token_authority.key(), + &[&token_authority.key()], + )?, + &[mint, multisig_token_authority, token_authority], + &[&[crate::TOKEN_AUTHORITY_SEED, &[token_authority_bump]]], + )?; Ok(()) } From 472fc49764fcb4a3e2b57adabbfdc1a4f0817599 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Feb 2025 20:19:21 -0500 Subject: [PATCH 18/20] solana: Make `additionalSigners` `readonly` --- solana/ts/lib/ntt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 105c4d710..26d0f2d59 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -874,7 +874,7 @@ export namespace NTT { config: NttBindings.Config, args: { currentMultisigAuthority: PublicKey; - additionalSigners: PublicKey[]; + additionalSigners: readonly PublicKey[]; multisigTokenAuthority?: PublicKey; }, pdas?: Pdas @@ -1023,7 +1023,7 @@ export namespace NTT { args: { rentPayer: PublicKey; newMultisigAuthority: PublicKey; - additionalSigners: PublicKey[]; + additionalSigners: readonly PublicKey[]; multisigTokenAuthority?: PublicKey; }, pdas?: Pdas From 98de06d25c9dfb446e623df683cd90e7f6d0fc7b Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 12 Feb 2025 17:17:38 -0500 Subject: [PATCH 19/20] solana: Refactor out`transfer_[ownership | token_authority]` into separate files --- .../src/instructions/admin/mod.rs | 202 +++++++++++ .../instructions/admin/transfer_ownership.rs | 148 ++++++++ .../transfer_token_authority.rs} | 342 +----------------- 3 files changed, 354 insertions(+), 338 deletions(-) create mode 100644 solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs create mode 100644 solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs rename solana/programs/example-native-token-transfers/src/instructions/{admin.rs => admin/transfer_token_authority.rs} (56%) diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs new file mode 100644 index 000000000..a8cb12c5e --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/mod.rs @@ -0,0 +1,202 @@ +use anchor_lang::prelude::*; +use ntt_messages::chain_id::ChainId; + +use crate::{ + config::Config, + peer::NttManagerPeer, + queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, + registered_transceiver::RegisteredTransceiver, +}; + +pub mod transfer_ownership; +pub mod transfer_token_authority; + +pub use transfer_ownership::*; +pub use transfer_token_authority::*; + +// * Set peers + +#[derive(Accounts)] +#[instruction(args: SetPeerArgs)] +pub struct SetPeer<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + pub owner: Signer<'info>, + + #[account( + has_one = owner, + )] + pub config: Account<'info, Config>, + + #[account( + init_if_needed, + space = 8 + NttManagerPeer::INIT_SPACE, + payer = payer, + seeds = [NttManagerPeer::SEED_PREFIX, args.chain_id.id.to_be_bytes().as_ref()], + bump + )] + pub peer: Account<'info, NttManagerPeer>, + + #[account( + init_if_needed, + space = 8 + InboxRateLimit::INIT_SPACE, + payer = payer, + seeds = [ + InboxRateLimit::SEED_PREFIX, + args.chain_id.id.to_be_bytes().as_ref() + ], + bump, + )] + pub inbox_rate_limit: Account<'info, InboxRateLimit>, + + pub system_program: Program<'info, System>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct SetPeerArgs { + pub chain_id: ChainId, + pub address: [u8; 32], + pub limit: u64, + /// The token decimals on the peer chain. + pub token_decimals: u8, +} + +pub fn set_peer(ctx: Context, args: SetPeerArgs) -> Result<()> { + ctx.accounts.peer.set_inner(NttManagerPeer { + bump: ctx.bumps.peer, + address: args.address, + token_decimals: args.token_decimals, + }); + + ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit { + bump: ctx.bumps.inbox_rate_limit, + rate_limit: RateLimitState::new(args.limit), + }); + Ok(()) +} + +// * Register transceivers + +#[derive(Accounts)] +pub struct RegisterTransceiver<'info> { + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account(executable)] + /// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be + /// used here that wraps the Transceiver account type. + pub transceiver: UncheckedAccount<'info>, + + #[account( + init, + space = 8 + RegisteredTransceiver::INIT_SPACE, + payer = payer, + seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()], + bump + )] + pub registered_transceiver: Account<'info, RegisteredTransceiver>, + + pub system_program: Program<'info, System>, +} + +pub fn register_transceiver(ctx: Context) -> Result<()> { + let id = ctx.accounts.config.next_transceiver_id; + ctx.accounts.config.next_transceiver_id += 1; + ctx.accounts + .registered_transceiver + .set_inner(RegisteredTransceiver { + bump: ctx.bumps.registered_transceiver, + id, + transceiver_address: ctx.accounts.transceiver.key(), + }); + + ctx.accounts.config.enabled_transceivers.set(id, true)?; + Ok(()) +} + +// * Limit rate adjustment + +#[derive(Accounts)] +pub struct SetOutboundLimit<'info> { + #[account( + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account(mut)] + pub rate_limit: Account<'info, OutboxRateLimit>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct SetOutboundLimitArgs { + pub limit: u64, +} + +pub fn set_outbound_limit( + ctx: Context, + args: SetOutboundLimitArgs, +) -> Result<()> { + ctx.accounts.rate_limit.set_limit(args.limit); + Ok(()) +} + +#[derive(Accounts)] +#[instruction(args: SetInboundLimitArgs)] +pub struct SetInboundLimit<'info> { + #[account( + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + #[account( + mut, + seeds = [ + InboxRateLimit::SEED_PREFIX, + args.chain_id.id.to_be_bytes().as_ref() + ], + bump = rate_limit.bump + )] + pub rate_limit: Account<'info, InboxRateLimit>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct SetInboundLimitArgs { + pub limit: u64, + pub chain_id: ChainId, +} + +pub fn set_inbound_limit(ctx: Context, args: SetInboundLimitArgs) -> Result<()> { + ctx.accounts.rate_limit.set_limit(args.limit); + Ok(()) +} + +// * Pausing + +#[derive(Accounts)] +pub struct SetPaused<'info> { + pub owner: Signer<'info>, + + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, +} + +pub fn set_paused(ctx: Context, paused: bool) -> Result<()> { + ctx.accounts.config.paused = paused; + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs new file mode 100644 index 000000000..e9ef8129e --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_ownership.rs @@ -0,0 +1,148 @@ +use anchor_lang::prelude::*; +use wormhole_solana_utils::cpi::bpf_loader_upgradeable::{self, BpfLoaderUpgradeable}; + +#[cfg(feature = "idl-build")] +use crate::messages::Hack; + +use crate::{config::Config, error::NTTError}; + +// * Transfer ownership + +/// For safety reasons, transferring ownership is a 2-step process. The first step is to set the +/// new owner, and the second step is for the new owner to claim the ownership. +/// This is to prevent a situation where the ownership is transferred to an +/// address that is not able to claim the ownership (by mistake). +/// +/// The transfer can be cancelled by the existing owner invoking the [`claim_ownership`] +/// instruction. +/// +/// Alternatively, the ownership can be transferred in a single step by calling the +/// [`transfer_ownership_one_step_unchecked`] instruction. This can be dangerous because if the new owner +/// cannot actually sign transactions (due to setting the wrong address), the program will be +/// permanently locked. If the intention is to transfer ownership to a program using this instruction, +/// take extra care to ensure that the owner is a PDA, not the program address itself. +#[derive(Accounts)] +pub struct TransferOwnership<'info> { + #[account( + mut, + has_one = owner, + )] + pub config: Account<'info, Config>, + + pub owner: Signer<'info>, + + /// CHECK: This account will be the signer in the [claim_ownership] instruction. + new_owner: UncheckedAccount<'info>, + + #[account( + seeds = [b"upgrade_lock"], + bump, + )] + /// CHECK: The seeds constraint enforces that this is the correct address + upgrade_lock: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable_program, + )] + program_data: Account<'info, ProgramData>, + + bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, +} + +pub fn transfer_ownership(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_owner = Some(ctx.accounts.new_owner.key()); + + // TODO: only transfer authority when the authority is not already the upgrade lock + bpf_loader_upgradeable::set_upgrade_authority_checked( + CpiContext::new_with_signer( + ctx.accounts + .bpf_loader_upgradeable_program + .to_account_info(), + bpf_loader_upgradeable::SetUpgradeAuthorityChecked { + program_data: ctx.accounts.program_data.to_account_info(), + current_authority: ctx.accounts.owner.to_account_info(), + new_authority: ctx.accounts.upgrade_lock.to_account_info(), + }, + &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], + ), + &crate::ID, + ) +} + +pub fn transfer_ownership_one_step_unchecked(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_owner = None; + ctx.accounts.config.owner = ctx.accounts.new_owner.key(); + + // NOTE: unlike in `transfer_ownership`, we use the unchecked version of the + // `set_upgrade_authority` instruction here. The checked version requires + // the new owner to be a signer, which is what we want to avoid here. + bpf_loader_upgradeable::set_upgrade_authority( + CpiContext::new( + ctx.accounts + .bpf_loader_upgradeable_program + .to_account_info(), + bpf_loader_upgradeable::SetUpgradeAuthority { + program_data: ctx.accounts.program_data.to_account_info(), + current_authority: ctx.accounts.owner.to_account_info(), + new_authority: Some(ctx.accounts.new_owner.to_account_info()), + }, + ), + &crate::ID, + ) +} + +// * Claim ownership + +#[derive(Accounts)] +pub struct ClaimOwnership<'info> { + #[account( + mut, + constraint = ( + config.pending_owner == Some(new_owner.key()) + || config.owner == new_owner.key() + ) @ NTTError::InvalidPendingOwner + )] + pub config: Account<'info, Config>, + + #[account( + seeds = [b"upgrade_lock"], + bump, + )] + /// CHECK: The seeds constraint enforces that this is the correct address + upgrade_lock: UncheckedAccount<'info>, + + pub new_owner: Signer<'info>, + + #[account( + mut, + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable_program, + )] + program_data: Account<'info, ProgramData>, + + bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, +} + +pub fn claim_ownership(ctx: Context) -> Result<()> { + ctx.accounts.config.pending_owner = None; + ctx.accounts.config.owner = ctx.accounts.new_owner.key(); + + bpf_loader_upgradeable::set_upgrade_authority_checked( + CpiContext::new_with_signer( + ctx.accounts + .bpf_loader_upgradeable_program + .to_account_info(), + bpf_loader_upgradeable::SetUpgradeAuthorityChecked { + program_data: ctx.accounts.program_data.to_account_info(), + current_authority: ctx.accounts.upgrade_lock.to_account_info(), + new_authority: ctx.accounts.new_owner.to_account_info(), + }, + &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], + ), + &crate::ID, + ) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs similarity index 56% rename from solana/programs/example-native-token-transfers/src/instructions/admin.rs rename to solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs index 82271bef9..c809331d5 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs @@ -1,163 +1,12 @@ use anchor_lang::prelude::*; use anchor_spl::{token_2022::spl_token_2022::instruction::AuthorityType, token_interface}; -use ntt_messages::chain_id::ChainId; -use wormhole_solana_utils::cpi::bpf_loader_upgradeable::{self, BpfLoaderUpgradeable}; - -#[cfg(feature = "idl-build")] -use crate::messages::Hack; use crate::{ - config::Config, - error::NTTError, - peer::NttManagerPeer, - pending_token_authority::PendingTokenAuthority, - queue::{inbox::InboxRateLimit, outbox::OutboxRateLimit, rate_limit::RateLimitState}, - registered_transceiver::RegisteredTransceiver, + config::Config, error::NTTError, pending_token_authority::PendingTokenAuthority, spl_multisig::SplMultisig, }; -// * Transfer ownership - -/// For safety reasons, transferring ownership is a 2-step process. The first step is to set the -/// new owner, and the second step is for the new owner to claim the ownership. -/// This is to prevent a situation where the ownership is transferred to an -/// address that is not able to claim the ownership (by mistake). -/// -/// The transfer can be cancelled by the existing owner invoking the [`claim_ownership`] -/// instruction. -/// -/// Alternatively, the ownership can be transferred in a single step by calling the -/// [`transfer_ownership_one_step_unchecked`] instruction. This can be dangerous because if the new owner -/// cannot actually sign transactions (due to setting the wrong address), the program will be -/// permanently locked. If the intention is to transfer ownership to a program using this instruction, -/// take extra care to ensure that the owner is a PDA, not the program address itself. -#[derive(Accounts)] -pub struct TransferOwnership<'info> { - #[account( - mut, - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - /// CHECK: This account will be the signer in the [claim_ownership] instruction. - new_owner: UncheckedAccount<'info>, - - #[account( - seeds = [b"upgrade_lock"], - bump, - )] - /// CHECK: The seeds constraint enforces that this is the correct address - upgrade_lock: UncheckedAccount<'info>, - - #[account( - mut, - seeds = [crate::ID.as_ref()], - bump, - seeds::program = bpf_loader_upgradeable_program, - )] - program_data: Account<'info, ProgramData>, - - bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, -} - -pub fn transfer_ownership(ctx: Context) -> Result<()> { - ctx.accounts.config.pending_owner = Some(ctx.accounts.new_owner.key()); - - // TODO: only transfer authority when the authority is not already the upgrade lock - bpf_loader_upgradeable::set_upgrade_authority_checked( - CpiContext::new_with_signer( - ctx.accounts - .bpf_loader_upgradeable_program - .to_account_info(), - bpf_loader_upgradeable::SetUpgradeAuthorityChecked { - program_data: ctx.accounts.program_data.to_account_info(), - current_authority: ctx.accounts.owner.to_account_info(), - new_authority: ctx.accounts.upgrade_lock.to_account_info(), - }, - &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], - ), - &crate::ID, - ) -} - -pub fn transfer_ownership_one_step_unchecked(ctx: Context) -> Result<()> { - ctx.accounts.config.pending_owner = None; - ctx.accounts.config.owner = ctx.accounts.new_owner.key(); - - // NOTE: unlike in `transfer_ownership`, we use the unchecked version of the - // `set_upgrade_authority` instruction here. The checked version requires - // the new owner to be a signer, which is what we want to avoid here. - bpf_loader_upgradeable::set_upgrade_authority( - CpiContext::new( - ctx.accounts - .bpf_loader_upgradeable_program - .to_account_info(), - bpf_loader_upgradeable::SetUpgradeAuthority { - program_data: ctx.accounts.program_data.to_account_info(), - current_authority: ctx.accounts.owner.to_account_info(), - new_authority: Some(ctx.accounts.new_owner.to_account_info()), - }, - ), - &crate::ID, - ) -} - -// * Claim ownership - -#[derive(Accounts)] -pub struct ClaimOwnership<'info> { - #[account( - mut, - constraint = ( - config.pending_owner == Some(new_owner.key()) - || config.owner == new_owner.key() - ) @ NTTError::InvalidPendingOwner - )] - pub config: Account<'info, Config>, - - #[account( - seeds = [b"upgrade_lock"], - bump, - )] - /// CHECK: The seeds constraint enforces that this is the correct address - upgrade_lock: UncheckedAccount<'info>, - - pub new_owner: Signer<'info>, - - #[account( - mut, - seeds = [crate::ID.as_ref()], - bump, - seeds::program = bpf_loader_upgradeable_program, - )] - program_data: Account<'info, ProgramData>, - - bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, -} - -pub fn claim_ownership(ctx: Context) -> Result<()> { - ctx.accounts.config.pending_owner = None; - ctx.accounts.config.owner = ctx.accounts.new_owner.key(); - - bpf_loader_upgradeable::set_upgrade_authority_checked( - CpiContext::new_with_signer( - ctx.accounts - .bpf_loader_upgradeable_program - .to_account_info(), - bpf_loader_upgradeable::SetUpgradeAuthorityChecked { - program_data: ctx.accounts.program_data.to_account_info(), - current_authority: ctx.accounts.upgrade_lock.to_account_info(), - new_authority: ctx.accounts.new_owner.to_account_info(), - }, - &[&[b"upgrade_lock", &[ctx.bumps.upgrade_lock]]], - ), - &crate::ID, - ) -} - -// * Set token authority +// * Accept token authority #[derive(Accounts)] pub struct AcceptTokenAuthorityBase<'info> { @@ -254,6 +103,8 @@ pub fn accept_token_authority_from_multisig<'info>( Ok(()) } +// * Set token authority + #[derive(Accounts)] pub struct SetTokenAuthority<'info> { #[account( @@ -533,188 +384,3 @@ fn claim_from_multisig_token_authority<'info>( )?; Ok(()) } - -// * Set peers - -#[derive(Accounts)] -#[instruction(args: SetPeerArgs)] -pub struct SetPeer<'info> { - #[account(mut)] - pub payer: Signer<'info>, - - pub owner: Signer<'info>, - - #[account( - has_one = owner, - )] - pub config: Account<'info, Config>, - - #[account( - init_if_needed, - space = 8 + NttManagerPeer::INIT_SPACE, - payer = payer, - seeds = [NttManagerPeer::SEED_PREFIX, args.chain_id.id.to_be_bytes().as_ref()], - bump - )] - pub peer: Account<'info, NttManagerPeer>, - - #[account( - init_if_needed, - space = 8 + InboxRateLimit::INIT_SPACE, - payer = payer, - seeds = [ - InboxRateLimit::SEED_PREFIX, - args.chain_id.id.to_be_bytes().as_ref() - ], - bump, - )] - pub inbox_rate_limit: Account<'info, InboxRateLimit>, - - pub system_program: Program<'info, System>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct SetPeerArgs { - pub chain_id: ChainId, - pub address: [u8; 32], - pub limit: u64, - /// The token decimals on the peer chain. - pub token_decimals: u8, -} - -pub fn set_peer(ctx: Context, args: SetPeerArgs) -> Result<()> { - ctx.accounts.peer.set_inner(NttManagerPeer { - bump: ctx.bumps.peer, - address: args.address, - token_decimals: args.token_decimals, - }); - - ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit { - bump: ctx.bumps.inbox_rate_limit, - rate_limit: RateLimitState::new(args.limit), - }); - Ok(()) -} - -// * Register transceivers - -#[derive(Accounts)] -pub struct RegisterTransceiver<'info> { - #[account( - mut, - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account(mut)] - pub payer: Signer<'info>, - - #[account(executable)] - /// CHECK: transceiver is meant to be a transceiver program. Arguably a `Program` constraint could be - /// used here that wraps the Transceiver account type. - pub transceiver: UncheckedAccount<'info>, - - #[account( - init, - space = 8 + RegisteredTransceiver::INIT_SPACE, - payer = payer, - seeds = [RegisteredTransceiver::SEED_PREFIX, transceiver.key().as_ref()], - bump - )] - pub registered_transceiver: Account<'info, RegisteredTransceiver>, - - pub system_program: Program<'info, System>, -} - -pub fn register_transceiver(ctx: Context) -> Result<()> { - let id = ctx.accounts.config.next_transceiver_id; - ctx.accounts.config.next_transceiver_id += 1; - ctx.accounts - .registered_transceiver - .set_inner(RegisteredTransceiver { - bump: ctx.bumps.registered_transceiver, - id, - transceiver_address: ctx.accounts.transceiver.key(), - }); - - ctx.accounts.config.enabled_transceivers.set(id, true)?; - Ok(()) -} - -// * Limit rate adjustment -#[derive(Accounts)] -pub struct SetOutboundLimit<'info> { - #[account( - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account(mut)] - pub rate_limit: Account<'info, OutboxRateLimit>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct SetOutboundLimitArgs { - pub limit: u64, -} - -pub fn set_outbound_limit( - ctx: Context, - args: SetOutboundLimitArgs, -) -> Result<()> { - ctx.accounts.rate_limit.set_limit(args.limit); - Ok(()) -} - -#[derive(Accounts)] -#[instruction(args: SetInboundLimitArgs)] -pub struct SetInboundLimit<'info> { - #[account( - has_one = owner, - )] - pub config: Account<'info, Config>, - - pub owner: Signer<'info>, - - #[account( - mut, - seeds = [ - InboxRateLimit::SEED_PREFIX, - args.chain_id.id.to_be_bytes().as_ref() - ], - bump = rate_limit.bump - )] - pub rate_limit: Account<'info, InboxRateLimit>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct SetInboundLimitArgs { - pub limit: u64, - pub chain_id: ChainId, -} - -pub fn set_inbound_limit(ctx: Context, args: SetInboundLimitArgs) -> Result<()> { - ctx.accounts.rate_limit.set_limit(args.limit); - Ok(()) -} - -// * Pausing -#[derive(Accounts)] -pub struct SetPaused<'info> { - pub owner: Signer<'info>, - - #[account( - mut, - has_one = owner, - )] - pub config: Account<'info, Config>, -} - -pub fn set_paused(ctx: Context, paused: bool) -> Result<()> { - ctx.accounts.config.paused = paused; - Ok(()) -} From f1a084aeffd9e2c1b42dca9b3f579baca0502525 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 14 Feb 2025 11:43:41 -0500 Subject: [PATCH 20/20] solana: Add test helper file and refactor code --- solana/tests/anchor.test.ts | 804 ++++++++++++---------------------- solana/tests/utils/helpers.ts | 645 +++++++++++++++++++++++++++ 2 files changed, 931 insertions(+), 518 deletions(-) create mode 100644 solana/tests/utils/helpers.ts diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index 22b7cb793..00004c973 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -13,7 +13,6 @@ import { encoding, serialize, serializePayload, - signSendWait as ssw, } from "@wormhole-foundation/sdk"; import * as testing from "@wormhole-foundation/sdk-definitions/testing"; import { @@ -22,15 +21,21 @@ import { getSolanaSignAndSendSigner, } from "@wormhole-foundation/sdk-solana"; import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; -import * as fs from "fs"; -import { DummyTransferHook } from "../ts/idl/1_0_0/ts/dummy_transfer_hook.js"; -import { getTransceiverProgram, IdlVersion, NTT } from "../ts/index.js"; -import { derivePda } from "../ts/lib/utils.js"; +import { IdlVersion, NTT, getTransceiverProgram } from "../ts/index.js"; import { SolanaNtt } from "../ts/sdk/index.js"; - -const solanaRootDir = `${__dirname}/../`; - +import { + TestDummyTransferHook, + TestHelper, + TestMint, + assert, + signSendWait, +} from "./utils/helpers.js"; + +/** + * Test Config Constants + */ +const SOLANA_ROOT_DIR = `${__dirname}/../`; const VERSION: IdlVersion = "3.0.0"; const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID; const GUARDIAN_KEY = @@ -41,84 +46,52 @@ const NTT_ADDRESS: anchor.web3.PublicKey = const WH_TRANSCEIVER_ADDRESS: anchor.web3.PublicKey = anchor.workspace.NttTransceiver.programId; -async function signSendWait( - chain: ChainContext, - txs: AsyncGenerator, - signer: Signer -) { - try { - await ssw(chain, txs, signer); - } catch (e) { - console.error(e); - } -} - +/** + * Test Helpers + */ +const $ = new TestHelper("confirmed", TOKEN_PROGRAM); +const testDummyTransferHook = new TestDummyTransferHook( + anchor.workspace.DummyTransferHook, + TOKEN_PROGRAM, + spl.ASSOCIATED_TOKEN_PROGRAM_ID +); +let testMint: TestMint; + +/** + * Wallet Config + */ +const payer = $.keypair.read(`${SOLANA_ROOT_DIR}/keys/test.json`); +const payerAddress = new SolanaAddress(payer.publicKey); + +/** + * Mint Config + */ +const mint = $.keypair.generate(); +const mintAuthority = $.keypair.generate(); + +/** + * Contract Config + */ const w = new Wormhole("Devnet", [SolanaPlatform], { chains: { Solana: { contracts: { coreBridge: CORE_BRIDGE_ADDRESS } } }, }); - -const remoteXcvr: ChainAddress = { - chain: "Ethereum", - address: new UniversalAddress( - encoding.bytes.encode("transceiver".padStart(32, "\0")) - ), -}; -const remoteMgr: ChainAddress = { - chain: "Ethereum", - address: new UniversalAddress( - encoding.bytes.encode("nttManager".padStart(32, "\0")) - ), -}; - -const payerSecretKey = Uint8Array.from( - JSON.parse( - fs.readFileSync(`${solanaRootDir}/keys/test.json`, { - encoding: "utf-8", - }) - ) -); -const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey); - -const owner = anchor.web3.Keypair.generate(); -const connection = new anchor.web3.Connection( - "http://localhost:8899", - "confirmed" -); - -// make sure we're using the exact same Connection obj for rpc const ctx: ChainContext<"Devnet", "Solana"> = w .getPlatform("Solana") - .getChain("Solana", connection); - -let tokenAccount: anchor.web3.PublicKey; - -const mint = anchor.web3.Keypair.generate(); - -const dummyTransferHook = anchor.workspace - .DummyTransferHook as anchor.Program; - -const [extraAccountMetaListPDA] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], - dummyTransferHook.programId -); - -const [counterPDA] = anchor.web3.PublicKey.findProgramAddressSync( - [Buffer.from("counter")], - dummyTransferHook.programId -); - -async function counterValue(): Promise { - const counter = await dummyTransferHook.account.counter.fetch(counterPDA); - return counter.count; -} - -const coreBridge = new SolanaWormholeCore("Devnet", "Solana", connection, { + .getChain("Solana", $.connection); // make sure we're using the exact same Connection object for rpc +const coreBridge = new SolanaWormholeCore("Devnet", "Solana", $.connection, { coreBridge: CORE_BRIDGE_ADDRESS, }); - +const remoteMgr: ChainAddress = $.chainAddress.generateFromValue( + "Ethereum", + "nttManager" +); +const remoteXcvr: ChainAddress = $.chainAddress.generateFromValue( + "Ethereum", + "transceiver" +); const nttTransceivers = { wormhole: getTransceiverProgram( - connection, + $.connection, WH_TRANSCEIVER_ADDRESS.toBase58(), VERSION ), @@ -128,194 +101,116 @@ describe("example-native-token-transfers", () => { let ntt: SolanaNtt<"Devnet", "Solana">; let signer: Signer; let sender: AccountAddress<"Solana">; - let tokenAddress: string; - let multisigTokenAuthority: anchor.web3.PublicKey; + let tokenAccount: anchor.web3.PublicKey; beforeAll(async () => { - try { - signer = await getSolanaSignAndSendSigner(connection, payer, { - //debug: true, - }); - sender = Wormhole.parseAddress("Solana", signer.address()); - - const extensions = [spl.ExtensionType.TransferHook]; - const mintLen = spl.getMintLen(extensions); - const lamports = await connection.getMinimumBalanceForRentExemption( - mintLen - ); - - const transaction = new anchor.web3.Transaction().add( - anchor.web3.SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: mint.publicKey, - space: mintLen, - lamports, - programId: TOKEN_PROGRAM, - }), - spl.createInitializeTransferHookInstruction( - mint.publicKey, - owner.publicKey, - dummyTransferHook.programId, - TOKEN_PROGRAM - ), - spl.createInitializeMintInstruction( - mint.publicKey, - 9, - owner.publicKey, - null, - TOKEN_PROGRAM - ) - ); - - const { blockhash } = await connection.getLatestBlockhash(); - - transaction.feePayer = payer.publicKey; - transaction.recentBlockhash = blockhash; - - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ - payer, - mint, - ]); - - tokenAccount = await spl.createAssociatedTokenAccount( - connection, - payer, - mint.publicKey, - payer.publicKey, - undefined, - TOKEN_PROGRAM, - spl.ASSOCIATED_TOKEN_PROGRAM_ID - ); - - await spl.mintTo( - connection, - payer, - mint.publicKey, - tokenAccount, - owner, - 10_000_000n, - undefined, - undefined, - TOKEN_PROGRAM - ); - - tokenAddress = mint.publicKey.toBase58(); - // create our contract client - ntt = new SolanaNtt( - "Devnet", - "Solana", - connection, - { - ...ctx.config.contracts, - ntt: { - token: tokenAddress, - manager: NTT_ADDRESS.toBase58(), - transceiver: { - wormhole: nttTransceivers["wormhole"].programId.toBase58(), - }, + signer = await getSolanaSignAndSendSigner($.connection, payer, { + //debug: true, + }); + sender = Wormhole.parseAddress("Solana", signer.address()); + + testMint = await TestMint.createWithTokenExtensions( + $.connection, + payer, + mint, + mintAuthority, + 9, + TOKEN_PROGRAM, + spl.ASSOCIATED_TOKEN_PROGRAM_ID, + { + extensions: [spl.ExtensionType.TransferHook], + preMintInitIxs: [ + spl.createInitializeTransferHookInstruction( + mint.publicKey, + mintAuthority.publicKey, + testDummyTransferHook.program.programId, + TOKEN_PROGRAM + ), + ], + } + ); + + tokenAccount = await testMint.mint( + payer, + payer.publicKey, + 10_000_000n, + mintAuthority + ); + + // create our contract client + ntt = new SolanaNtt( + "Devnet", + "Solana", + $.connection, + { + ...ctx.config.contracts, + ntt: { + token: testMint.address.toBase58(), + manager: NTT_ADDRESS.toBase58(), + transceiver: { + wormhole: nttTransceivers["wormhole"].programId.toBase58(), }, }, - VERSION - ); - } catch (e) { - console.error("Failed to setup solana token: ", e); - throw e; - } + }, + VERSION + ); }); describe("Burning", () => { + let multisigTokenAuthority: anchor.web3.PublicKey; + beforeAll(async () => { - try { - multisigTokenAuthority = await spl.createMultisig( - connection, - payer, - [owner.publicKey, ntt.pdas.tokenAuthority()], - 1, - anchor.web3.Keypair.generate(), - undefined, - TOKEN_PROGRAM - ); - await spl.setAuthority( - connection, - payer, - mint.publicKey, - owner, - spl.AuthorityType.MintTokens, - multisigTokenAuthority, - [], - undefined, - TOKEN_PROGRAM - ); + // set multisigTokenAuthority as mint authority + multisigTokenAuthority = await $.multisig.create(payer, 1, [ + mintAuthority.publicKey, + ntt.pdas.tokenAuthority(), + ]); + await testMint.setMintAuthority( + payer, + multisigTokenAuthority, + mintAuthority + ); - // init - const initTxs = ntt.initialize(sender, { - mint: mint.publicKey, - outboundLimit: 1000000n, - mode: "burning", - multisig: multisigTokenAuthority, - }); - await signSendWait(ctx, initTxs, signer); + // init + const initTxs = ntt.initialize(sender, { + mint: testMint.address, + outboundLimit: 1_000_000n, + mode: "burning", + multisig: multisigTokenAuthority, + }); + await signSendWait(ctx, initTxs, signer); - // register - const registerTxs = ntt.registerWormholeTransceiver({ - payer: new SolanaAddress(payer.publicKey), - owner: new SolanaAddress(payer.publicKey), - }); - await signSendWait(ctx, registerTxs, signer); + // register Wormhole xcvr + const registerTxs = ntt.registerWormholeTransceiver({ + payer: payerAddress, + owner: payerAddress, + }); + await signSendWait(ctx, registerTxs, signer); - // set Wormhole xcvr peer - const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer( - remoteXcvr, - sender - ); - await signSendWait(ctx, setXcvrPeerTxs, signer); - - // set manager peer - const setPeerTxs = ntt.setPeer(remoteMgr, 18, 1000000n, sender); - await signSendWait(ctx, setPeerTxs, signer); - } catch (e) { - console.error("Failed to setup peer: ", e); - throw e; - } + // set Wormhole xcvr peer + const setXcvrPeerTxs = ntt.setWormholeTransceiverPeer(remoteXcvr, sender); + await signSendWait(ctx, setXcvrPeerTxs, signer); + + // set manager peer + const setPeerTxs = ntt.setPeer(remoteMgr, 18, 1_000_000n, sender); + await signSendWait(ctx, setPeerTxs, signer); }); it("Create ExtraAccountMetaList Account", async () => { - const initializeExtraAccountMetaListInstruction = - await dummyTransferHook.methods - .initializeExtraAccountMetaList() - .accountsStrict({ - payer: payer.publicKey, - mint: mint.publicKey, - counter: counterPDA, - extraAccountMetaList: extraAccountMetaListPDA, - tokenProgram: TOKEN_PROGRAM, - associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: anchor.web3.SystemProgram.programId, - }) - .instruction(); - - const transaction = new anchor.web3.Transaction().add( - initializeExtraAccountMetaListInstruction - ); - transaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - - transaction.sign(payer); - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + await testDummyTransferHook.extraAccountMetaList.initialize( + $.connection, payer, - ]); + testMint.address + ); }); it("Can send tokens", async () => { - const amount = 100000n; - const sender = Wormhole.parseAddress("Solana", signer.address()); - + const amount = 100_000n; const receiver = testing.utils.makeUniversalChainAddress("Ethereum"); // TODO: keep or remove the `outboxItem` param? // added as a way to keep tests the same but it technically breaks the Ntt interface - const outboxItem = anchor.web3.Keypair.generate(); + const outboxItem = $.keypair.generate(); const xferTxs = ntt.transfer( sender, amount, @@ -333,11 +228,11 @@ describe("example-native-token-transfers", () => { Object.keys(nttTransceivers).length ); - const wormholeMessage = derivePda( - ["message", outboxItem.publicKey.toBytes()], - nttTransceivers["wormhole"].programId + const wormholeXcvr = await ntt.getWormholeTransceiver(); + expect(wormholeXcvr).toBeTruthy(); + const wormholeMessage = wormholeXcvr!.pdas.wormholeMessageAccount( + outboxItem.publicKey ); - const unsignedVaa = await coreBridge.parsePostMessageAccount( wormholeMessage ); @@ -350,330 +245,223 @@ describe("example-native-token-transfers", () => { // assert that amount is what we expect expect( transceiverMessage.nttManagerPayload.payload.trimmedAmount - ).toMatchObject({ amount: 10000n, decimals: 8 }); + ).toMatchObject({ amount: 10_000n, decimals: 8 }); // get from balance - const balance = await connection.getTokenAccountBalance(tokenAccount); - expect(balance.value.amount).toBe("9900000"); + await assert.tokenBalance($.connection, tokenAccount).equal(9_900_000); }); describe("Can transfer mint authority to-and-from NTT manager", () => { - const newAuthority = anchor.web3.Keypair.generate(); + const newAuthority = $.keypair.generate(); let newMultisigAuthority: anchor.web3.PublicKey; + const nttOwner = payer.publicKey; beforeAll(async () => { - newMultisigAuthority = await spl.createMultisig( - connection, - payer, - [owner.publicKey, newAuthority.publicKey], - 2, - anchor.web3.Keypair.generate(), - undefined, - TOKEN_PROGRAM - ); + newMultisigAuthority = await $.multisig.create(payer, 2, [ + mintAuthority.publicKey, + newAuthority.publicKey, + ]); }); it("Fails when contract is not paused", async () => { - try { - const transaction = new anchor.web3.Transaction().add( - await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( - ntt.program, - await ntt.getConfig(), - { - owner: new SolanaAddress(await ntt.getOwner()).unwrap(), - newAuthority: newAuthority.publicKey, - multisigTokenAuthority, - } + await assert + .promise( + $.sendAndConfirm( + await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( + ntt.program, + await ntt.getConfig(), + { + owner: nttOwner, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer ) - ); - transaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ - payer, - ]); - // tx should fail so this expect should never be hit - expect(false).toBeTruthy(); - } catch (e) { - expect(e).toBeInstanceOf(anchor.web3.SendTransactionError); - const parsedError = anchor.AnchorError.parse( - (e as anchor.web3.SendTransactionError).logs ?? [] - ); - expect(parsedError?.error.errorCode).toEqual({ + ) + .failsWithAnchorError(anchor.web3.SendTransactionError, { code: "NotPaused", number: 6024, }); - } finally { - const mintInfo = await spl.getMint( - connection, - mint.publicKey, - undefined, - TOKEN_PROGRAM - ); - expect(mintInfo.mintAuthority).toEqual(multisigTokenAuthority); - } + + await assert.testMintAuthority(testMint).equal(multisigTokenAuthority); }); test("Multisig(owner, TA) -> newAuthority", async () => { // retry after pausing contract - const pauseTxs = await ntt.pause(new SolanaAddress(payer.publicKey)); + const pauseTxs = ntt.pause(payerAddress); await signSendWait(ctx, pauseTxs, signer); - const transaction = new anchor.web3.Transaction().add( + await $.sendAndConfirm( await NTT.createSetTokenAuthorityOneStepUncheckedInstruction( ntt.program, await ntt.getConfig(), { - owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + owner: nttOwner, newAuthority: newAuthority.publicKey, multisigTokenAuthority, } - ) + ), + payer ); - transaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ - payer, - ]); - const mintInfo = await spl.getMint( - connection, - mint.publicKey, - undefined, - TOKEN_PROGRAM - ); - expect(mintInfo.mintAuthority).toEqual(newAuthority.publicKey); + await assert.testMintAuthority(testMint).equal(newAuthority.publicKey); }); test("newAuthority -> TA", async () => { - const transaction = new anchor.web3.Transaction().add( + await $.sendAndConfirm( await NTT.createAcceptTokenAuthorityInstruction( ntt.program, await ntt.getConfig(), { currentAuthority: newAuthority.publicKey, } - ) - ); - transaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + ), payer, - newAuthority, - ]); - - const mintInfo = await spl.getMint( - connection, - mint.publicKey, - undefined, - TOKEN_PROGRAM + newAuthority ); - expect(mintInfo.mintAuthority).toEqual(ntt.pdas.tokenAuthority()); + + await assert + .testMintAuthority(testMint) + .equal(ntt.pdas.tokenAuthority()); }); test("TA -> Multisig(owner, newAuthority)", async () => { // set token authority: TA -> newMultisigAuthority - const setTransaction = new anchor.web3.Transaction().add( + await $.sendAndConfirm( await NTT.createSetTokenAuthorityInstruction( ntt.program, await ntt.getConfig(), { - rentPayer: new SolanaAddress(await ntt.getOwner()).unwrap(), - owner: new SolanaAddress(await ntt.getOwner()).unwrap(), + rentPayer: nttOwner, + owner: nttOwner, newAuthority: newMultisigAuthority, } - ) - ); - setTransaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - setTransaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction( - connection, - setTransaction, - [payer] + ), + payer ); // claim token authority: newMultisigAuthority <- TA - const claimTransaction = new anchor.web3.Transaction().add( + await $.sendAndConfirm( await NTT.createClaimTokenAuthorityToMultisigInstruction( ntt.program, await ntt.getConfig(), { - rentPayer: new SolanaAddress(await ntt.getOwner()).unwrap(), + rentPayer: nttOwner, newMultisigAuthority, - additionalSigners: [newAuthority.publicKey, owner.publicKey], + additionalSigners: [ + newAuthority.publicKey, + mintAuthority.publicKey, + ], } - ) - ); - claimTransaction.feePayer = payer.publicKey; - claimTransaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction( - connection, - claimTransaction, - [payer, newAuthority, owner] + ), + payer, + newAuthority, + mintAuthority ); - const mintInfo = await spl.getMint( - connection, - mint.publicKey, - undefined, - TOKEN_PROGRAM - ); - expect(mintInfo.mintAuthority).toEqual(newMultisigAuthority); + await assert.testMintAuthority(testMint).equal(newMultisigAuthority); }); test("Multisig(owner, newAuthority) -> Multisig(owner, TA)", async () => { - const transaction = new anchor.web3.Transaction().add( + await $.sendAndConfirm( await NTT.createAcceptTokenAuthorityFromMultisigInstruction( ntt.program, await ntt.getConfig(), { currentMultisigAuthority: newMultisigAuthority, - additionalSigners: [newAuthority.publicKey, owner.publicKey], + additionalSigners: [ + newAuthority.publicKey, + mintAuthority.publicKey, + ], multisigTokenAuthority, } - ) - ); - transaction.feePayer = payer.publicKey; - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + ), payer, newAuthority, - owner, - ]); - - const mintInfo = await spl.getMint( - connection, - mint.publicKey, - undefined, - TOKEN_PROGRAM + mintAuthority ); - expect(mintInfo.mintAuthority).toEqual(multisigTokenAuthority); + + await assert.testMintAuthority(testMint).equal(multisigTokenAuthority); }); it("Fails on claim after revert", async () => { - try { - // fund newAuthority for it to be rent payer - const signature = await connection.requestAirdrop( - newAuthority.publicKey, - anchor.web3.LAMPORTS_PER_SOL - ); - const { blockhash, lastValidBlockHeight } = - await connection.getLatestBlockhash(); - await connection.confirmTransaction({ - blockhash, - lastValidBlockHeight, - signature, - }); - let newAuthorityBalance = ( - await connection.getAccountInfo(newAuthority.publicKey) - )?.lamports; - expect(newAuthorityBalance).toBe(anchor.web3.LAMPORTS_PER_SOL); - - // set token authority: multisigTokenAuthority -> newAuthority - const setTransaction = new anchor.web3.Transaction().add( - await NTT.createSetTokenAuthorityInstruction( - ntt.program, - await ntt.getConfig(), - { - rentPayer: newAuthority.publicKey, - owner: new SolanaAddress(await ntt.getOwner()).unwrap(), - newAuthority: newAuthority.publicKey, - multisigTokenAuthority, - } - ) - ); - setTransaction.feePayer = payer.publicKey; - setTransaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction( - connection, - setTransaction, - [payer, newAuthority] + // fund newAuthority for it to be rent payer + await $.airdrop(newAuthority.publicKey, anchor.web3.LAMPORTS_PER_SOL); + await assert + .nativeBalance($.connection, newAuthority.publicKey) + .equal(anchor.web3.LAMPORTS_PER_SOL); + + // set token authority: multisigTokenAuthority -> newAuthority + await $.sendAndConfirm( + await NTT.createSetTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + owner: nttOwner, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer, + newAuthority + ); + const pendingTokenAuthorityRentExemptAmount = + await $.connection.getMinimumBalanceForRentExemption( + ntt.program.account.pendingTokenAuthority.size ); - newAuthorityBalance = ( - await connection.getAccountInfo(newAuthority.publicKey) - )?.lamports; - const pendingTokenAuthorityRentExemptAmount = - await connection.getMinimumBalanceForRentExemption( - ntt.program.account.pendingTokenAuthority.size - ); - expect(newAuthorityBalance).toBe( + await assert + .nativeBalance($.connection, newAuthority.publicKey) + .equal( anchor.web3.LAMPORTS_PER_SOL - pendingTokenAuthorityRentExemptAmount ); - // revert token authority: multisigTokenAuthority - const revertTransaction = new anchor.web3.Transaction().add( - await NTT.createRevertTokenAuthorityInstruction( - ntt.program, - await ntt.getConfig(), - { - rentPayer: newAuthority.publicKey, - owner: new SolanaAddress(await ntt.getOwner()).unwrap(), - multisigTokenAuthority, - } - ) - ); - revertTransaction.feePayer = payer.publicKey; - revertTransaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction( - connection, - revertTransaction, - [payer] - ); - newAuthorityBalance = ( - await connection.getAccountInfo(newAuthority.publicKey) - )?.lamports; - expect(newAuthorityBalance).toBe(anchor.web3.LAMPORTS_PER_SOL); - - // claim token authority: newAuthority <- multisigTokenAuthority - const claimTransaction = new anchor.web3.Transaction().add( - await NTT.createClaimTokenAuthorityInstruction( - ntt.program, - await ntt.getConfig(), - { - rentPayer: newAuthority.publicKey, - newAuthority: newAuthority.publicKey, - multisigTokenAuthority, - } + // revert token authority: multisigTokenAuthority + await $.sendAndConfirm( + await NTT.createRevertTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + owner: nttOwner, + multisigTokenAuthority, + } + ), + payer + ); + await assert + .nativeBalance($.connection, newAuthority.publicKey) + .equal(anchor.web3.LAMPORTS_PER_SOL); + + // claim token authority: newAuthority <- multisigTokenAuthority + await assert + .promise( + $.sendAndConfirm( + await NTT.createClaimTokenAuthorityInstruction( + ntt.program, + await ntt.getConfig(), + { + rentPayer: newAuthority.publicKey, + newAuthority: newAuthority.publicKey, + multisigTokenAuthority, + } + ), + payer, + newAuthority ) - ); - claimTransaction.feePayer = payer.publicKey; - claimTransaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction( - connection, - claimTransaction, - [payer, newAuthority] - ); - // tx should fail so this expect should never be hit - expect(false).toBeTruthy(); - } catch (e) { - expect(e).toBeInstanceOf(anchor.web3.SendTransactionError); - const parsedError = anchor.AnchorError.parse( - (e as anchor.web3.SendTransactionError).logs ?? [] - ); - expect(parsedError?.error.errorCode).toEqual({ + ) + .failsWithAnchorError(anchor.web3.SendTransactionError, { code: "AccountNotInitialized", number: 3012, }); - } finally { - const mintInfo = await spl.getMint( - connection, - mint.publicKey, - undefined, - TOKEN_PROGRAM - ); - expect(mintInfo.mintAuthority).toEqual(multisigTokenAuthority); - } + + await assert.testMintAuthority(testMint).equal(multisigTokenAuthority); }); afterAll(async () => { // unpause - const unpauseTxs = await ntt.unpause( - new SolanaAddress(payer.publicKey) - ); + const unpauseTxs = ntt.unpause(payerAddress); await signSendWait(ctx, unpauseTxs, signer); }); }); @@ -686,7 +474,6 @@ describe("example-native-token-transfers", () => { ); const guardians = new testing.mocks.MockGuardians(0, [GUARDIAN_KEY]); - const sender = Wormhole.parseAddress("Solana", signer.address()); const sendingTransceiverMessage = { sourceNttManager: remoteMgr.address as UniversalAddress, @@ -698,7 +485,7 @@ describe("example-native-token-transfers", () => { sender: new UniversalAddress("FACE".padStart(64, "0")), payload: { trimmedAmount: { - amount: 10000n, + amount: 10_000n, decimals: 8, }, sourceToken: new UniversalAddress("FAFA".padStart(64, "0")), @@ -718,40 +505,20 @@ describe("example-native-token-transfers", () => { const rawVaa = guardians.addSignatures(published, [0]); const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); const redeemTxs = ntt.redeem([vaa], sender, multisigTokenAuthority); - try { - await signSendWait(ctx, redeemTxs, signer); - } catch (e) { - console.error(e); - throw e; - } + await signSendWait(ctx, redeemTxs, signer); - expect((await counterValue()).toString()).toEqual("2"); + assert.bn(await testDummyTransferHook.counter.value()).equal(2); }); it("Can mint independently", async () => { - const dest = await spl.getOrCreateAssociatedTokenAccount( - connection, + const temp = await testMint.mint( payer, - mint.publicKey, - anchor.web3.Keypair.generate().publicKey, - false, - undefined, - undefined, - TOKEN_PROGRAM - ); - await spl.mintTo( - connection, - payer, - mint.publicKey, - dest.address, - multisigTokenAuthority, + $.keypair.generate().publicKey, 1, - [owner], - undefined, - TOKEN_PROGRAM + multisigTokenAuthority, + mintAuthority ); - const balance = await connection.getTokenAccountBalance(dest.address); - expect(balance.value.amount.toString()).toBe("1"); + await assert.tokenBalance($.connection, temp).equal(1); }); }); @@ -760,7 +527,7 @@ describe("example-native-token-transfers", () => { const ctx = wh.getChain("Solana"); const overrides = { Solana: { - token: tokenAddress, + token: mint.publicKey.toBase58(), manager: NTT_ADDRESS.toBase58(), transceiver: { wormhole: nttTransceivers["wormhole"].programId.toBase58(), @@ -770,7 +537,7 @@ describe("example-native-token-transfers", () => { describe("ABI Versions Test", () => { test("It initializes from Rpc", async () => { - const ntt = await SolanaNtt.fromRpc(connection, { + const ntt = await SolanaNtt.fromRpc($.connection, { Solana: { ...ctx.config, contracts: { @@ -783,7 +550,7 @@ describe("example-native-token-transfers", () => { }); test("It initializes from constructor", async () => { - const ntt = new SolanaNtt("Devnet", "Solana", connection, { + const ntt = new SolanaNtt("Devnet", "Solana", $.connection, { ...ctx.config.contracts, ...{ ntt: overrides["Solana"] }, }); @@ -792,9 +559,9 @@ describe("example-native-token-transfers", () => { test("It gets the correct version", async () => { const version = await SolanaNtt.getVersion( - connection, + $.connection, { ntt: overrides["Solana"] }, - new SolanaAddress(payer.publicKey.toBase58()) + payerAddress ); expect(version).toBe("3.0.0"); }); @@ -807,7 +574,7 @@ describe("example-native-token-transfers", () => { .emitterAccount() .toBase58(); - const ntt = new SolanaNtt("Devnet", "Solana", connection, { + const ntt = new SolanaNtt("Devnet", "Solana", $.connection, { ...ctx.config.contracts, ...{ ntt: overrideEmitter }, }); @@ -815,13 +582,14 @@ describe("example-native-token-transfers", () => { }); test("It gets the correct transceiver type", async () => { - const ntt = new SolanaNtt("Devnet", "Solana", connection, { + const ntt = new SolanaNtt("Devnet", "Solana", $.connection, { ...ctx.config.contracts, ...{ ntt: overrides["Solana"] }, }); const whTransceiver = await ntt.getWormholeTransceiver(); + expect(whTransceiver).toBeTruthy(); const transceiverType = await whTransceiver!.getTransceiverType( - new SolanaAddress(payer.publicKey.toBase58()) + payerAddress ); expect(transceiverType).toBe("wormhole"); }); diff --git a/solana/tests/utils/helpers.ts b/solana/tests/utils/helpers.ts new file mode 100644 index 000000000..784fe7b70 --- /dev/null +++ b/solana/tests/utils/helpers.ts @@ -0,0 +1,645 @@ +import * as anchor from "@coral-xyz/anchor"; +import * as spl from "@solana/spl-token"; +import * as fs from "fs"; +import { + Chain, + ChainAddress, + ChainContext, + encoding, + Signer, + signSendWait as ssw, + UniversalAddress, +} from "@wormhole-foundation/sdk"; +import { DummyTransferHook } from "../../ts/idl/1_0_0/ts/dummy_transfer_hook.js"; +import { derivePda } from "../../ts/lib/utils.js"; + +export interface ErrorConstructor { + new (...args: any[]): Error; +} + +/** + * Assertion utility functions + */ +export const assert = { + /** + * Asserts BN + * @param actual BN to compare against + */ + bn: (actual: anchor.BN) => ({ + /** + * Asserts `actual` equals `expected` + * @param expected BN to compare with + */ + equal: (expected: anchor.BN | number | string | bigint) => { + expect( + actual.eq( + expected instanceof anchor.BN + ? expected + : new anchor.BN(expected.toString()) + ) + ).toBeTruthy(); + }, + }), + + /** + * Asserts mint authority for given `mint` + * @param connection Connection to use + * @param mint Mint account + * @param tokenProgram SPL Token program account + */ + mintAuthority: ( + connection: anchor.web3.Connection, + mint: anchor.web3.PublicKey, + tokenProgram = spl.TOKEN_2022_PROGRAM_ID + ) => ({ + /** + * Asserts queried mint authority equals `expectedAuthority` + * @param expectedAuthority Expected mint authority + */ + equal: async (expectedAuthority: anchor.web3.PublicKey) => { + const mintInfo = await spl.getMint( + connection, + mint, + undefined, + tokenProgram + ); + expect(mintInfo.mintAuthority).toEqual(expectedAuthority); + }, + }), + + /** + * Asserts mint authority for given `testMint` + * @param testMint `TestMint` object to query to fetch mintAuthority + */ + testMintAuthority: (testMint: TestMint) => ({ + /** + * Asserts queried mint authority equals `expectedAuthority` + * @param expectedAuthority Expected mint authority + */ + equal: async (expectedAuthority: anchor.web3.PublicKey) => { + const mintInfo = await testMint.getMint(); + expect(mintInfo.mintAuthority).toEqual(expectedAuthority); + }, + }), + + /** + * Asserts native balance for given `publicKey` + * @param connection Connection to use + * @param publicKey Account to query to fetch native balance + * @returns + */ + nativeBalance: ( + connection: anchor.web3.Connection, + publicKey: anchor.web3.PublicKey + ) => ({ + /** + * Asserts queried native balance equals `expectedBalance` + * @param expectedBalance Expected lamports balance + */ + equal: async (expectedBalance: anchor.BN | number | string | bigint) => { + const balance = await connection.getAccountInfo(publicKey); + expect(balance?.lamports.toString()).toBe(expectedBalance.toString()); + }, + }), + + /** + * Asserts token balance for given `tokenAccount` + * @param connection Connection to use + * @param tokenAccount Token account to query to fetch token balance + */ + tokenBalance: ( + connection: anchor.web3.Connection, + tokenAccount: anchor.web3.PublicKey + ) => ({ + /** + * Asserts queried token balance equals `expectedBalance` + * @param expectedBalance Expected token balance + */ + equal: async (expectedBalance: anchor.BN | number | string | bigint) => { + const balance = await connection.getTokenAccountBalance(tokenAccount); + expect(balance.value.amount).toBe(expectedBalance.toString()); + }, + }), + + /** + * Asserts promise fails and throws expected error + * @param prom Promise to execute (intended to fail) + */ + promise: (prom: Promise) => ({ + /** + * Asserts promise throws error of type `errorType` + * @param errorType Expected type for thrown error + */ + fails: async (errorType?: ErrorConstructor) => { + let result: any; + try { + result = await prom; + } catch (error: any) { + if (errorType != null) { + expect(error).toBeInstanceOf(errorType); + } + return; + } + throw new Error(`Promise did not fail. Result: ${result}`); + }, + /** + * Asserts promise throws error containing `message` + * @param message Expected message contained in thrown error + */ + failsWith: async (message: string) => { + let result: any; + try { + result = await prom; + } catch (error: any) { + const errorStr: string = error.toString(); + if (errorStr.includes(message)) { + return; + } + throw { + message: "Error does not contain the asked message", + stack: errorStr, + }; + } + throw new Error(`Promise did not fail. Result: ${result}`); + }, + /** + * Asserts promise throws Anchor error coreesponding to type `errorType` and `errorCode` + * @param errorType Expected type for thrown error + * @param errorCode Expected error code for thrown error + */ + failsWithAnchorError: async ( + errorType: ErrorConstructor, + errorCode: typeof anchor.AnchorError.prototype.error.errorCode + ) => { + let result: any; + try { + result = await prom; + } catch (error: any) { + expect(error).toBeInstanceOf(errorType); + const parsedError = anchor.AnchorError.parse(error.logs ?? []); + expect(parsedError?.error.errorCode).toEqual(errorCode); + return; + } + throw new Error(`Promise did not fail. Result: ${result}`); + }, + }), +}; + +/** + * General test utility class + */ +export class TestHelper { + static readonly LOCALHOST = "http://localhost:8899"; + readonly connection: anchor.web3.Connection; + + constructor( + readonly finality: anchor.web3.Finality = "confirmed", + readonly tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID + ) { + this.connection = new anchor.web3.Connection( + TestHelper.LOCALHOST, + finality + ); + } + + /** + * `Keypair` utility functions + */ + keypair = { + /** + * Wrapper around `Keypair.generate()` + * @returns Generated `Keypair` + */ + generate: () => anchor.web3.Keypair.generate(), + /** + * Reads secret key file and returns `Keypair` it corresponds to + * @param path File path containing secret key + * @returns Corresponding `Keypair` + */ + read: (path: string) => + this.keypair.from( + JSON.parse(fs.readFileSync(path, { encoding: "utf8" })) + ), + /** + * Wrapper around `Keypair.fromSecretKey` for number array-like + * @param bytes Number array-like corresponding to a secret key + * @returns Corresponding `Keypair` + */ + from: (bytes: number[]) => + anchor.web3.Keypair.fromSecretKey(Uint8Array.from(bytes)), + }; + + /** + * `ChainAddress` utility functions + */ + chainAddress = { + /** + * Generates a `ChainAddress` by encoding value to pass off as `UniversalAddress` + * @param chain `Chain` to generate `ChainAddress` for + * @param value String to use for generating `UniversalAddress` + * @returns Generated `ChainAddress` + */ + generateFromValue: (chain: Chain, value: string): ChainAddress => ({ + chain, + address: new UniversalAddress( + encoding.bytes.encode(value.padStart(32, "\0")) + ), + }), + }; + + /** + * SPL Multisig utility functions + */ + multisig = { + /** + * Wrapper around `spl.createMultisig` + * @param payer Payer of the transaction and initialization fees + * @param m Number of required signatures + * @param signers Full set of signers + * @returns Address of the new multisig + */ + create: async ( + payer: anchor.web3.Signer, + m: number, + signers: anchor.web3.PublicKey[] + ) => { + return spl.createMultisig( + this.connection, + payer, + signers, + m, + this.keypair.generate(), + undefined, + this.tokenProgram + ); + }, + }; + + /** + * Wrapper around `confirmTransaction` + * @param signature Signature of transaction to confirm + * @returns Result of signature confirmation + */ + confirm = async (signature: anchor.web3.TransactionSignature) => { + const { blockhash, lastValidBlockHeight } = + await this.connection.getLatestBlockhash(); + return this.connection.confirmTransaction({ + blockhash, + lastValidBlockHeight, + signature, + }); + }; + + /** + * Wrapper around `sendAndConfirm` for `this.connection` + * @param ixs Instruction(s)/transaction used to create the transaction + * @param payer Payer of the transaction fees + * @param signers Signing accounts required by the transaction + * @returns Signature of the confirmed transaction + */ + sendAndConfirm = async ( + ixs: + | anchor.web3.TransactionInstruction + | anchor.web3.Transaction + | Array, + payer: anchor.web3.Signer, + ...signers: anchor.web3.Signer[] + ): Promise => { + return sendAndConfirm(this.connection, ixs, payer, ...signers); + }; + + /** + * Wrapper around `requestAirdrop()` + * @param to Recipient account for airdrop + * @param lamports Amount in lamports to airdrop + * @returns + */ + airdrop = async (to: anchor.web3.PublicKey, lamports: number) => { + return this.confirm(await this.connection.requestAirdrop(to, lamports)); + }; +} + +/** + * Mint-related test utility class + */ +export class TestMint { + private constructor( + readonly connection: anchor.web3.Connection, + readonly address: anchor.web3.PublicKey, + readonly decimals: number, + readonly tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID, + readonly associatedTokenProgram: anchor.web3.PublicKey = spl.ASSOCIATED_TOKEN_PROGRAM_ID + ) {} + + /** + * Creates and initializes a new mint + * @param connection Connection to use + * @param payer Payer of the transaction and initialization fees + * @param authority Account that will control minting + * @param decimals Location of the decimal place + * @param tokenProgram SPL Token program account + * @param associatedTokenProgram SPL Associated Token program account + * @returns new `TestMint` object initialized with the created mint + */ + static create = async ( + connection: anchor.web3.Connection, + payer: anchor.web3.Signer, + authority: anchor.web3.Signer, + decimals: number, + tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: anchor.web3.PublicKey = spl.ASSOCIATED_TOKEN_PROGRAM_ID + ) => { + return new TestMint( + connection, + await spl.createMint( + connection, + payer, + authority.publicKey, + null, + decimals, + undefined, + undefined, + tokenProgram + ), + decimals, + tokenProgram, + associatedTokenProgram + ); + }; + + /** + * Creates and initializes a new mint with Token Extensions + * @param connection Connection to use + * @param payer Payer of the transaction and initialization fees + * @param mint Keypair of mint to be created + * @param authority Account that will control minting + * @param decimals Location of the decimal place + * @param tokenProgram SPL Token program account + * @param associatedTokenProgram SPL Associated Token program account + * @param extensionArgs.extensions Token extensions mint is to be initialized with + * @param extensionArgs.additionalDataLength Additional space to allocate for extension + * @param extensionArgs.preMintInitIxs Instructions to execute before `InitializeMint` instruction + * @param extensionArgs.postMintInitIxs Instructions to execute after `InitializeMint` instruction + * @returns new `TestMint` object initialized with the created mint + */ + static createWithTokenExtensions = async ( + connection: anchor.web3.Connection, + payer: anchor.web3.Signer, + mint: anchor.web3.Keypair, + authority: anchor.web3.Signer, + decimals: number, + tokenProgram: anchor.web3.PublicKey = spl.TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: anchor.web3.PublicKey = spl.ASSOCIATED_TOKEN_PROGRAM_ID, + extensionArgs: { + extensions: spl.ExtensionType[]; + additionalDataLength?: number; + preMintInitIxs?: anchor.web3.TransactionInstruction[]; + postMintInitIxs?: anchor.web3.TransactionInstruction[]; + } + ) => { + const mintLen = spl.getMintLen(extensionArgs.extensions); + const additionalDataLength = extensionArgs.additionalDataLength ?? 0; + const lamports = await connection.getMinimumBalanceForRentExemption( + mintLen + additionalDataLength + ); + await sendAndConfirm( + connection, + [ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports, + programId: tokenProgram, + }), + ...(extensionArgs.preMintInitIxs ?? []), + spl.createInitializeMintInstruction( + mint.publicKey, + decimals, + authority.publicKey, + null, + tokenProgram + ), + ...(extensionArgs.postMintInitIxs ?? []), + ], + payer, + mint + ); + + return new TestMint( + connection, + mint.publicKey, + decimals, + tokenProgram, + associatedTokenProgram + ); + }; + + /** + * Wrapper around `spl.getMint` + * @returns Mint information + */ + getMint = async () => { + return spl.getMint( + this.connection, + this.address, + undefined, + this.tokenProgram + ); + }; + + /** + * Creates ATA for `accountOwner` and mints `amount` tokens to it + * @param payer Payer of the transaction and initialization fees + * @param accountOwner Owner of token account + * @param amount Amount to mint + * @param mintAuthority Minting authority + * @param multiSigners Signing accounts if `mintAuthority` is a multisig + * @returns Address of ATA + */ + mint = async ( + payer: anchor.web3.Signer, + accountOwner: anchor.web3.PublicKey, + amount: number | bigint, + mintAuthority: anchor.web3.Signer | anchor.web3.PublicKey, + ...multiSigners: anchor.web3.Signer[] + ) => { + const tokenAccount = await spl.getOrCreateAssociatedTokenAccount( + this.connection, + payer, + this.address, + accountOwner, + false, + undefined, + undefined, + this.tokenProgram, + this.associatedTokenProgram + ); + + await spl.mintTo( + this.connection, + payer, + this.address, + tokenAccount.address, + mintAuthority, + amount, + multiSigners, + undefined, + this.tokenProgram + ); + + return tokenAccount.address; + }; + + /** + * Wrapper around `spl.setAuthority` for `spl.AuthorityType.MintTokens` + * @param payer Payer of the transaction fees + * @param newAuthority New mint authority + * @param currentAuthority Current mint authority + * @param multiSigners Signing accounts if `currentAuthority` is a multisig + * @returns Signature of the confirmed transaction + */ + setMintAuthority = async ( + payer: anchor.web3.Signer, + newAuthority: anchor.web3.PublicKey, + currentAuthority: anchor.web3.Signer | anchor.web3.PublicKey, + ...multiSigners: anchor.web3.Signer[] + ) => { + return spl.setAuthority( + this.connection, + payer, + this.address, + currentAuthority, + spl.AuthorityType.MintTokens, + newAuthority, + multiSigners, + undefined, + this.tokenProgram + ); + }; +} + +/** + * Dummy Transfer Hook program related test utility class + */ +export class TestDummyTransferHook { + constructor( + readonly program: anchor.Program, + readonly tokenProgram = spl.TOKEN_2022_PROGRAM_ID, + readonly associatedTokenProgram = spl.ASSOCIATED_TOKEN_PROGRAM_ID + ) {} + + /** + * Counter utility functions + */ + counter = { + /** + * @returns Counter PDA + */ + pda: () => derivePda(["counter"], this.program.programId), + + /** + * Queries counter and returns counter count + * @returns Queried counter value + */ + value: async () => { + const counter = await this.program.account.counter.fetch( + this.counter.pda() + ); + return counter.count; + }, + }; + + /** + * Extra Account Meta List utility functions + */ + extraAccountMetaList = { + /** + * @param mint Mint account + * @returns Extra Account Meta List PDA + */ + pda: (mint: anchor.web3.PublicKey) => + derivePda( + ["extra-account-metas", mint.toBytes()], + this.program.programId + ), + /** + * Initializes Extra Account Meta List account + * @param connection Connection to use + * @param payer Payer of the transaction fees + * @param mint Mint account + * @returns Signature of the confirmed transaction + */ + initialize: async ( + connection: anchor.web3.Connection, + payer: anchor.web3.Signer, + mint: anchor.web3.PublicKey + ) => { + return sendAndConfirm( + connection, + await this.program.methods + .initializeExtraAccountMetaList() + .accountsStrict({ + payer: payer.publicKey, + mint, + counter: this.counter.pda(), + extraAccountMetaList: this.extraAccountMetaList.pda(mint), + tokenProgram: this.tokenProgram, + associatedTokenProgram: this.associatedTokenProgram, + systemProgram: anchor.web3.SystemProgram.programId, + }) + .instruction(), + payer + ); + }, + }; +} + +/** + * Try-catch wrapper around `signSendWait` + * @param chain Chain to execute transaction on + * @param txs Generator of unsigned transactions + * @param signer Signing account required by the transactions + */ +export const signSendWait = async ( + chain: ChainContext, + txs: AsyncGenerator, + signer: Signer +) => { + try { + await ssw(chain, txs, signer); + } catch (e) { + console.error(e); + } +}; + +/** + * Wrapper around `sendAndConfirmTransaction` + * @param connection Connection to use + * @param ixs Instruction(s)/transaction used to create the transaction + * @param payer Payer of the transaction fees + * @param signers Signing accounts required by the transaction + * @returns Signature of the confirmed transaction + */ +export const sendAndConfirm = async ( + connection: anchor.web3.Connection, + ixs: + | anchor.web3.TransactionInstruction + | anchor.web3.Transaction + | Array, + payer: anchor.web3.Signer, + ...signers: anchor.web3.Signer[] +): Promise => { + const { value } = await connection.getLatestBlockhashAndContext(); + const tx = new anchor.web3.Transaction({ + ...value, + feePayer: payer.publicKey, + }).add(...(Array.isArray(ixs) ? ixs : [ixs])); + + return anchor.web3.sendAndConfirmTransaction( + connection, + tx, + [payer, ...signers], + {} + ); +};