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

Merged
merged 20 commits into from
Feb 22, 2025
Merged
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
246873e
Update `accept_token_authority` to account for multisig token authority
nvsriram Jan 31, 2025
cb1b70e
Add `accept_token_authority_multisig` ix
nvsriram Jan 31, 2025
a4501a6
Update `set_token_authority_one_step_unchecked` to account for multis…
nvsriram Jan 31, 2025
11056ac
Update `set_token_authority` to account for multisig token authority
nvsriram Jan 31, 2025
010c311
Update `claim_token_authority`and add `claim_token_authority_to_multi…
nvsriram Jan 31, 2025
1dbb6c6
solana: Rename ix to `acceptTokenAuthorityToMultisig`
nvsriram Feb 3, 2025
d6358f4
solana: Pass all remaining_accounts as required signers in `accept_to…
nvsriram Feb 3, 2025
e9204d1
solana: Fix lint
nvsriram Feb 3, 2025
927de5e
solana: Add TS helper functions
nvsriram Feb 3, 2025
5c6d910
solana: Update IDL
nvsriram Feb 3, 2025
924a91d
solana: Add token authority transfer test cases
nvsriram Feb 3, 2025
4840f3a
solana: Make comment/function syntax consistent
nvsriram Feb 3, 2025
6ac5876
solana: Fix unneeded borrow
nvsriram Feb 3, 2025
e2d0c46
solana: Replace if let with match syntax
nvsriram Feb 5, 2025
f59c581
solana: Simplify constraint using map_or
nvsriram Feb 5, 2025
cdfc4c1
solana: Add comment on lack of custom error thrown
nvsriram Feb 5, 2025
b043851
solana: Refactor out `claim_from_(multisig_)token_authority` fn's
nvsriram Feb 5, 2025
6de946e
solana: Make `additionalSigners` `readonly`
nvsriram Feb 6, 2025
d67f705
solana: Refactor out`transfer_[ownership | token_authority]` into sep…
nvsriram Feb 12, 2025
baefdb3
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