diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin.rs b/solana/programs/example-native-token-transfers/src/instructions/admin.rs
deleted file mode 100644
index e5c87d7f5..000000000
--- a/solana/programs/example-native-token-transfers/src/instructions/admin.rs
+++ /dev/null
@@ -1,546 +0,0 @@
-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,
-};
-
-// * 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<TransferOwnership>) -> 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<TransferOwnership>) -> 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<ClaimOwnership>) -> 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
-
-#[derive(Accounts)]
-pub struct AcceptTokenAuthority<'info> {
-    #[account(
-        has_one = mint,
-        constraint = config.paused @ NTTError::NotPaused,
-    )]
-    pub config: Account<'info, Config>,
-
-    #[account(mut)]
-    pub mint: InterfaceAccount<'info, token_interface::Mint>,
-
-    #[account(
-        seeds = [crate::TOKEN_AUTHORITY_SEED],
-        bump,
-    )]
-    /// CHECK: The constraints enforce this is valid mint authority
-    pub token_authority: UncheckedAccount<'info>,
-
-    pub current_authority: Signer<'info>,
-
-    pub token_program: Interface<'info, token_interface::TokenInterface>,
-}
-
-pub fn accept_token_authority(ctx: Context<AcceptTokenAuthority>) -> Result<()> {
-    token_interface::set_authority(
-        CpiContext::new(
-            ctx.accounts.token_program.to_account_info(),
-            token_interface::SetAuthority {
-                account_or_mint: ctx.accounts.mint.to_account_info(),
-                current_authority: ctx.accounts.current_authority.to_account_info(),
-            },
-        ),
-        AuthorityType::MintTokens,
-        Some(ctx.accounts.token_authority.key()),
-    )
-}
-
-#[derive(Accounts)]
-pub struct SetTokenAuthority<'info> {
-    #[account(
-        has_one = owner,
-        has_one = mint,
-        constraint = config.paused @ NTTError::NotPaused,
-    )]
-    pub config: Account<'info, Config>,
-
-    pub owner: Signer<'info>,
-
-    #[account(mut)]
-    pub mint: InterfaceAccount<'info, token_interface::Mint>,
-
-    #[account(
-        seeds = [crate::TOKEN_AUTHORITY_SEED],
-        bump,
-    )]
-    /// CHECK: The constraints enforce this is valid mint authority
-    pub token_authority: UncheckedAccount<'info>,
-
-    /// CHECK: This account will be the signer in the [claim_token_authority] instruction.
-    pub new_authority: UncheckedAccount<'info>,
-}
-
-#[derive(Accounts)]
-pub struct SetTokenAuthorityChecked<'info> {
-    #[account(
-        constraint = common.token_authority.key() == common.mint.mint_authority.unwrap() @ NTTError::InvalidMintAuthority
-    )]
-    pub common: SetTokenAuthority<'info>,
-
-    #[account(mut)]
-    pub rent_payer: Signer<'info>,
-
-    #[account(
-        init_if_needed,
-        space = 8 + PendingTokenAuthority::INIT_SPACE,
-        payer = rent_payer,
-        seeds = [PendingTokenAuthority::SEED_PREFIX],
-        bump
-     )]
-    pub pending_token_authority: Account<'info, PendingTokenAuthority>,
-
-    pub system_program: Program<'info, System>,
-}
-
-pub fn set_token_authority(ctx: Context<SetTokenAuthorityChecked>) -> Result<()> {
-    ctx.accounts
-        .pending_token_authority
-        .set_inner(PendingTokenAuthority {
-            bump: ctx.bumps.pending_token_authority,
-            pending_authority: ctx.accounts.common.new_authority.key(),
-            rent_payer: ctx.accounts.rent_payer.key(),
-        });
-    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<SetTokenAuthorityUnchecked>,
-) -> 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)]
-pub struct ClaimTokenAuthorityBase<'info> {
-    #[account(
-        has_one = mint,
-        constraint = config.paused @ NTTError::NotPaused,
-    )]
-    pub config: Account<'info, Config>,
-
-    #[account(mut)]
-    pub mint: InterfaceAccount<'info, token_interface::Mint>,
-
-    #[account(
-        seeds = [crate::TOKEN_AUTHORITY_SEED],
-        bump,
-    )]
-    /// CHECK: The seeds constraint enforces that this is the correct address
-    pub token_authority: UncheckedAccount<'info>,
-
-    #[account(mut)]
-    /// CHECK: the `pending_token_authority` constraint enforces that this is the correct address
-    pub rent_payer: UncheckedAccount<'info>,
-
-    #[account(
-        mut,
-        seeds = [PendingTokenAuthority::SEED_PREFIX],
-        bump = pending_token_authority.bump,
-        has_one = rent_payer @ NTTError::IncorrectRentPayer,
-        close = rent_payer
-     )]
-    pub pending_token_authority: Account<'info, PendingTokenAuthority>,
-
-    pub token_program: Interface<'info, token_interface::TokenInterface>,
-
-    pub system_program: Program<'info, System>,
-}
-
-#[derive(Accounts)]
-pub struct RevertTokenAuthority<'info> {
-    pub common: ClaimTokenAuthorityBase<'info>,
-
-    #[account(
-        address = common.config.owner
-    )]
-    pub owner: Signer<'info>,
-}
-
-pub fn revert_token_authority(_ctx: Context<RevertTokenAuthority>) -> Result<()> {
-    Ok(())
-}
-
-#[derive(Accounts)]
-pub struct ClaimTokenAuthority<'info> {
-    pub common: ClaimTokenAuthorityBase<'info>,
-
-    #[account(
-        address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority
-    )]
-    pub new_authority: Signer<'info>,
-}
-
-pub fn claim_token_authority(ctx: Context<ClaimTokenAuthority>) -> 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(),
-            },
-            &[&[
-                crate::TOKEN_AUTHORITY_SEED,
-                &[ctx.bumps.common.token_authority],
-            ]],
-        ),
-        AuthorityType::MintTokens,
-        Some(ctx.accounts.new_authority.key()),
-    )
-}
-
-// * 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<SetPeer>, 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<RegisterTransceiver>) -> 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<SetOutboundLimit>,
-    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<SetInboundLimit>, 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<SetPaused>, paused: bool) -> Result<()> {
-    ctx.accounts.config.paused = paused;
-    Ok(())
-}
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<SetPeer>, 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<RegisterTransceiver>) -> 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<SetOutboundLimit>,
+    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<SetInboundLimit>, 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<SetPaused>, 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<TransferOwnership>) -> 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<TransferOwnership>) -> 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<ClaimOwnership>) -> 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/transfer_token_authority.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs
new file mode 100644
index 000000000..c809331d5
--- /dev/null
+++ b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs
@@ -0,0 +1,386 @@
+use anchor_lang::prelude::*;
+use anchor_spl::{token_2022::spl_token_2022::instruction::AuthorityType, token_interface};
+
+use crate::{
+    config::Config, error::NTTError, pending_token_authority::PendingTokenAuthority,
+    spl_multisig::SplMultisig,
+};
+
+// * Accept token authority
+
+#[derive(Accounts)]
+pub struct AcceptTokenAuthorityBase<'info> {
+    #[account(
+        has_one = mint,
+        constraint = config.paused @ NTTError::NotPaused,
+    )]
+    pub config: Account<'info, Config>,
+
+    #[account(mut)]
+    pub mint: InterfaceAccount<'info, token_interface::Mint>,
+
+    #[account(
+        seeds = [crate::TOKEN_AUTHORITY_SEED],
+        bump,
+    )]
+    /// 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<InterfaceAccount<'info, SplMultisig>>,
+
+    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<AcceptTokenAuthority>) -> Result<()> {
+    token_interface::set_authority(
+        CpiContext::new(
+            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.current_authority.to_account_info(),
+            },
+        ),
+        AuthorityType::MintTokens,
+        Some(match &ctx.accounts.common.multisig_token_authority {
+            Some(multisig_token_authority) => multisig_token_authority.key(),
+            None => ctx.accounts.common.token_authority.key(),
+        }),
+    )
+}
+
+#[derive(Accounts)]
+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 = 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![
+        ctx.accounts.common.mint.to_account_info(),
+        new_authority.clone(),
+        ctx.accounts.current_multisig_authority.to_account_info(),
+    ];
+
+    // pass ctx.remaining_accounts as required signers
+    {
+        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(),
+            &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(())
+}
+
+// * Set token authority
+
+#[derive(Accounts)]
+pub struct SetTokenAuthority<'info> {
+    #[account(
+        has_one = owner,
+        has_one = mint,
+        constraint = config.paused @ NTTError::NotPaused,
+    )]
+    pub config: Account<'info, Config>,
+
+    pub owner: Signer<'info>,
+
+    #[account(mut)]
+    pub mint: InterfaceAccount<'info, token_interface::Mint>,
+
+    #[account(
+        seeds = [crate::TOKEN_AUTHORITY_SEED],
+        bump,
+    )]
+    /// 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<InterfaceAccount<'info, SplMultisig>>,
+
+    /// 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<SetTokenAuthorityUnchecked>,
+) -> Result<()> {
+    match &ctx.accounts.common.multisig_token_authority {
+        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)]
+pub struct SetTokenAuthorityChecked<'info> {
+    #[account(
+        constraint =
+            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>,
+
+    #[account(mut)]
+    pub rent_payer: Signer<'info>,
+
+    #[account(
+        init_if_needed,
+        space = 8 + PendingTokenAuthority::INIT_SPACE,
+        payer = rent_payer,
+        seeds = [PendingTokenAuthority::SEED_PREFIX],
+        bump
+     )]
+    pub pending_token_authority: Account<'info, PendingTokenAuthority>,
+
+    pub system_program: Program<'info, System>,
+}
+
+pub fn set_token_authority(ctx: Context<SetTokenAuthorityChecked>) -> Result<()> {
+    ctx.accounts
+        .pending_token_authority
+        .set_inner(PendingTokenAuthority {
+            bump: ctx.bumps.pending_token_authority,
+            pending_authority: ctx.accounts.common.new_authority.key(),
+            rent_payer: ctx.accounts.rent_payer.key(),
+        });
+    Ok(())
+}
+
+// * Claim token authority
+
+#[derive(Accounts)]
+pub struct ClaimTokenAuthorityBase<'info> {
+    #[account(
+        has_one = mint,
+        constraint = config.paused @ NTTError::NotPaused,
+    )]
+    pub config: Account<'info, Config>,
+
+    #[account(mut)]
+    pub mint: InterfaceAccount<'info, token_interface::Mint>,
+
+    #[account(
+        seeds = [crate::TOKEN_AUTHORITY_SEED],
+        bump,
+    )]
+    /// 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<InterfaceAccount<'info, SplMultisig>>,
+
+    #[account(mut)]
+    /// CHECK: the `pending_token_authority` constraint enforces that this is the correct address
+    pub rent_payer: UncheckedAccount<'info>,
+
+    #[account(
+        mut,
+        seeds = [PendingTokenAuthority::SEED_PREFIX],
+        bump = pending_token_authority.bump,
+        has_one = rent_payer @ NTTError::IncorrectRentPayer,
+        close = rent_payer
+     )]
+    pub pending_token_authority: Account<'info, PendingTokenAuthority>,
+
+    pub token_program: Interface<'info, token_interface::TokenInterface>,
+
+    pub system_program: Program<'info, System>,
+}
+
+#[derive(Accounts)]
+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>,
+}
+
+pub fn revert_token_authority(_ctx: Context<RevertTokenAuthority>) -> Result<()> {
+    Ok(())
+}
+
+#[derive(Accounts)]
+pub struct ClaimTokenAuthority<'info> {
+    pub common: ClaimTokenAuthorityBase<'info>,
+
+    #[account(
+        address = common.pending_token_authority.pending_authority @ NTTError::InvalidPendingTokenAuthority
+    )]
+    pub new_authority: Signer<'info>,
+}
+
+pub fn claim_token_authority(ctx: Context<ClaimTokenAuthority>) -> Result<()> {
+    match &ctx.accounts.common.multisig_token_authority {
+        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)]
+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<ClaimTokenAuthorityToMultisig>,
+) -> 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,
+        )?;
+    }
+
+    match &ctx.accounts.common.multisig_token_authority {
+        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(())
+}
diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs
index 7b480b498..f3aed6dfb 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<SetTokenAuthorityChecked>) -> Result<()> {
-        instructions::set_token_authority(ctx)
+    pub fn accept_token_authority_from_multisig<'info>(
+        ctx: Context<'_, '_, '_, 'info, AcceptTokenAuthorityFromMultisig<'info>>,
+    ) -> Result<()> {
+        instructions::accept_token_authority_from_multisig(ctx)
     }
 
     pub fn set_token_authority_one_step_unchecked(
@@ -156,6 +158,10 @@ pub mod example_native_token_transfers {
         instructions::set_token_authority_one_step_unchecked(ctx)
     }
 
+    pub fn set_token_authority(ctx: Context<SetTokenAuthorityChecked>) -> Result<()> {
+        instructions::set_token_authority(ctx)
+    }
+
     pub fn revert_token_authority(ctx: Context<RevertTokenAuthority>) -> Result<()> {
         instructions::revert_token_authority(ctx)
     }
@@ -164,6 +170,12 @@ pub mod example_native_token_transfers {
         instructions::claim_token_authority(ctx)
     }
 
+    pub fn claim_token_authority_to_multisig(
+        ctx: Context<ClaimTokenAuthorityToMultisig>,
+    ) -> Result<()> {
+        instructions::claim_token_authority_to_multisig(ctx)
+    }
+
     pub fn set_paused(ctx: Context<SetPaused>, pause: bool) -> Result<()> {
         instructions::set_paused(ctx, pause)
     }
diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts
index a582890b5..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<any, any, any>,
-  txs: AsyncGenerator<any>,
-  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<DummyTransferHook>;
-
-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<anchor.BN> {
-  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 multisig: anchor.web3.PublicKey;
-  let tokenAddress: string;
+  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 {
-        multisig = 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,
-          multisig,
-          [],
-          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,
-        });
-        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
+      );
     });
 
-    test("Can send tokens", async () => {
-      const amount = 100000n;
-      const sender = Wormhole.parseAddress("Solana", signer.address());
-
+    it("Can send tokens", async () => {
+      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,11 +245,225 @@ 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 = $.keypair.generate();
+      let newMultisigAuthority: anchor.web3.PublicKey;
+      const nttOwner = payer.publicKey;
+
+      beforeAll(async () => {
+        newMultisigAuthority = await $.multisig.create(payer, 2, [
+          mintAuthority.publicKey,
+          newAuthority.publicKey,
+        ]);
+      });
+
+      it("Fails when contract is not paused", async () => {
+        await assert
+          .promise(
+            $.sendAndConfirm(
+              await NTT.createSetTokenAuthorityOneStepUncheckedInstruction(
+                ntt.program,
+                await ntt.getConfig(),
+                {
+                  owner: nttOwner,
+                  newAuthority: newAuthority.publicKey,
+                  multisigTokenAuthority,
+                }
+              ),
+              payer
+            )
+          )
+          .failsWithAnchorError(anchor.web3.SendTransactionError, {
+            code: "NotPaused",
+            number: 6024,
+          });
+
+        await assert.testMintAuthority(testMint).equal(multisigTokenAuthority);
+      });
+
+      test("Multisig(owner, TA) -> newAuthority", async () => {
+        // retry after pausing contract
+        const pauseTxs = ntt.pause(payerAddress);
+        await signSendWait(ctx, pauseTxs, signer);
+
+        await $.sendAndConfirm(
+          await NTT.createSetTokenAuthorityOneStepUncheckedInstruction(
+            ntt.program,
+            await ntt.getConfig(),
+            {
+              owner: nttOwner,
+              newAuthority: newAuthority.publicKey,
+              multisigTokenAuthority,
+            }
+          ),
+          payer
+        );
+
+        await assert.testMintAuthority(testMint).equal(newAuthority.publicKey);
+      });
+
+      test("newAuthority -> TA", async () => {
+        await $.sendAndConfirm(
+          await NTT.createAcceptTokenAuthorityInstruction(
+            ntt.program,
+            await ntt.getConfig(),
+            {
+              currentAuthority: newAuthority.publicKey,
+            }
+          ),
+          payer,
+          newAuthority
+        );
+
+        await assert
+          .testMintAuthority(testMint)
+          .equal(ntt.pdas.tokenAuthority());
+      });
+
+      test("TA -> Multisig(owner, newAuthority)", async () => {
+        // set token authority: TA -> newMultisigAuthority
+        await $.sendAndConfirm(
+          await NTT.createSetTokenAuthorityInstruction(
+            ntt.program,
+            await ntt.getConfig(),
+            {
+              rentPayer: nttOwner,
+              owner: nttOwner,
+              newAuthority: newMultisigAuthority,
+            }
+          ),
+          payer
+        );
+
+        // claim token authority: newMultisigAuthority <- TA
+        await $.sendAndConfirm(
+          await NTT.createClaimTokenAuthorityToMultisigInstruction(
+            ntt.program,
+            await ntt.getConfig(),
+            {
+              rentPayer: nttOwner,
+              newMultisigAuthority,
+              additionalSigners: [
+                newAuthority.publicKey,
+                mintAuthority.publicKey,
+              ],
+            }
+          ),
+          payer,
+          newAuthority,
+          mintAuthority
+        );
+
+        await assert.testMintAuthority(testMint).equal(newMultisigAuthority);
+      });
+
+      test("Multisig(owner, newAuthority) -> Multisig(owner, TA)", async () => {
+        await $.sendAndConfirm(
+          await NTT.createAcceptTokenAuthorityFromMultisigInstruction(
+            ntt.program,
+            await ntt.getConfig(),
+            {
+              currentMultisigAuthority: newMultisigAuthority,
+              additionalSigners: [
+                newAuthority.publicKey,
+                mintAuthority.publicKey,
+              ],
+              multisigTokenAuthority,
+            }
+          ),
+          payer,
+          newAuthority,
+          mintAuthority
+        );
+
+        await assert.testMintAuthority(testMint).equal(multisigTokenAuthority);
+      });
+
+      it("Fails on claim after revert", async () => {
+        // 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
+          );
+        await assert
+          .nativeBalance($.connection, newAuthority.publicKey)
+          .equal(
+            anchor.web3.LAMPORTS_PER_SOL - pendingTokenAuthorityRentExemptAmount
+          );
+
+        // 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
+            )
+          )
+          .failsWithAnchorError(anchor.web3.SendTransactionError, {
+            code: "AccountNotInitialized",
+            number: 3012,
+          });
+
+        await assert.testMintAuthority(testMint).equal(multisigTokenAuthority);
+      });
+
+      afterAll(async () => {
+        // unpause
+        const unpauseTxs = ntt.unpause(payerAddress);
+        await signSendWait(ctx, unpauseTxs, signer);
+      });
     });
 
     it("Can receive tokens", async () => {
@@ -365,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,
@@ -377,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")),
@@ -396,41 +504,21 @@ 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);
-      try {
-        await signSendWait(ctx, redeemTxs, signer);
-      } catch (e) {
-        console.error(e);
-        throw e;
-      }
+      const redeemTxs = ntt.redeem([vaa], sender, multisigTokenAuthority);
+      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,
-        payer,
-        mint.publicKey,
-        anchor.web3.Keypair.generate().publicKey,
-        false,
-        undefined,
-        undefined,
-        TOKEN_PROGRAM
-      );
-      await spl.mintTo(
-        connection,
+      const temp = await testMint.mint(
         payer,
-        mint.publicKey,
-        dest.address,
-        multisig,
+        $.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);
     });
   });
 
@@ -439,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(),
@@ -447,9 +535,9 @@ describe("example-native-token-transfers", () => {
       },
     };
 
-    describe("ABI Versions Test", function () {
-      test("It initializes from Rpc", async function () {
-        const ntt = await SolanaNtt.fromRpc(connection, {
+    describe("ABI Versions Test", () => {
+      test("It initializes from Rpc", async () => {
+        const ntt = await SolanaNtt.fromRpc($.connection, {
           Solana: {
             ...ctx.config,
             contracts: {
@@ -461,24 +549,24 @@ describe("example-native-token-transfers", () => {
         expect(ntt).toBeTruthy();
       });
 
-      test("It initializes from constructor", async function () {
-        const ntt = new SolanaNtt("Devnet", "Solana", connection, {
+      test("It initializes from constructor", async () => {
+        const ntt = new SolanaNtt("Devnet", "Solana", $.connection, {
           ...ctx.config.contracts,
           ...{ ntt: overrides["Solana"] },
         });
         expect(ntt).toBeTruthy();
       });
 
-      test("It gets the correct version", async function () {
+      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");
       });
 
-      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"])
         );
@@ -486,21 +574,22 @@ 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 },
         });
         expect(ntt).toBeTruthy();
       });
 
-      test("It gets the correct transceiver type", async function () {
-        const ntt = new SolanaNtt("Devnet", "Solana", connection, {
+      test("It gets the correct transceiver type", async () => {
+        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<unknown>) => ({
+    /**
+     * 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<anchor.web3.TransactionInstruction>,
+    payer: anchor.web3.Signer,
+    ...signers: anchor.web3.Signer[]
+  ): Promise<anchor.web3.TransactionSignature> => {
+    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<DummyTransferHook>,
+    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<any, any, any>,
+  txs: AsyncGenerator<any>,
+  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<anchor.web3.TransactionInstruction>,
+  payer: anchor.web3.Signer,
+  ...signers: anchor.web3.Signer[]
+): Promise<anchor.web3.TransactionSignature> => {
+  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],
+    {}
+  );
+};
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": []
diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts
index 4aed49162..26d0f2d59 100644
--- a/solana/ts/lib/ntt.ts
+++ b/solana/ts/lib/ntt.ts
@@ -849,6 +849,7 @@ export namespace NTT {
     config: NttBindings.Config<IdlVersion>,
     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<NttBindings.NativeTokenTransfer<IdlVersion>>,
+    config: NttBindings.Config<IdlVersion>,
+    args: {
+      currentMultisigAuthority: PublicKey;
+      additionalSigners: readonly 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<NttBindings.NativeTokenTransfer<IdlVersion>>,
+    config: NttBindings.Config<IdlVersion>,
+    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<NttBindings.NativeTokenTransfer<IdlVersion>>,
     config: NttBindings.Config<IdlVersion>,
@@ -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<NttBindings.NativeTokenTransfer<IdlVersion>>,
+    config: NttBindings.Config<IdlVersion>,
+    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<NttBindings.NativeTokenTransfer<IdlVersion>>,
+    config: NttBindings.Config<IdlVersion>,
+    args: {
+      rentPayer: PublicKey;
+      newMultisigAuthority: PublicKey;
+      additionalSigners: readonly 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<NttBindings.NativeTokenTransfer<IdlVersion>>,
     args: {