Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solana: Handle transferring mint authority using SPL Multisig #587

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bb7a3f4
Update `accept_token_authority` to account for multisig token authority
nvsriram Jan 31, 2025
e944553
Add `accept_token_authority_multisig` ix
nvsriram Jan 31, 2025
f269001
Update `set_token_authority_one_step_unchecked` to account for multis…
nvsriram Jan 31, 2025
960f080
Update `set_token_authority` to account for multisig token authority
nvsriram Jan 31, 2025
a31365a
Update `claim_token_authority`and add `claim_token_authority_to_multi…
nvsriram Jan 31, 2025
2fc9fcb
solana: Rename ix to `acceptTokenAuthorityToMultisig`
nvsriram Feb 3, 2025
69aa742
solana: Pass all remaining_accounts as required signers in `accept_to…
nvsriram Feb 3, 2025
f102b3a
solana: Fix lint
nvsriram Feb 3, 2025
d82f87e
solana: Add TS helper functions
nvsriram Feb 3, 2025
ad25ae1
solana: Update IDL
nvsriram Feb 3, 2025
fbda72a
solana: Add token authority transfer test cases
nvsriram Feb 3, 2025
3f20aa9
solana: Make comment/function syntax consistent
nvsriram Feb 3, 2025
6e5d603
solana: Fix unneeded borrow
nvsriram Feb 3, 2025
7932667
solana: Replace if let with match syntax
nvsriram Feb 5, 2025
08c750f
solana: Simplify constraint using map_or
nvsriram Feb 5, 2025
5e7b92c
solana: Add comment on lack of custom error thrown
nvsriram Feb 5, 2025
a33f8df
solana: Refactor out `claim_from_(multisig_)token_authority` fn's
nvsriram Feb 5, 2025
472fc49
solana: Make `additionalSigners` `readonly`
nvsriram Feb 6, 2025
98de06d
solana: Refactor out`transfer_[ownership | token_authority]` into sep…
nvsriram Feb 12, 2025
f1a084a
solana: Add test helper file and refactor code
nvsriram Feb 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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(())
}
Original file line number Diff line number Diff line change
@@ -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,
)
}
Loading
Loading