Skip to content

Commit

Permalink
solana: Add SPL multisig support (#568)
Browse files Browse the repository at this point in the history
* `initialize_multisig` and `release_inbound_multisig_mint` to act as multisig variants of initialize and release_inbound_mint respectively
* Add SPLMultisig `InterfaceAccount` wrapper
* Refactor out common structs and function logic to avoid duplication
* Update test to verify independent minting capability after transferring authority to multisig
* Update IDL
  • Loading branch information
nvsriram authored Jan 20, 2025
1 parent dcc0305 commit 3311787
Show file tree
Hide file tree
Showing 10 changed files with 979 additions and 89 deletions.
2 changes: 2 additions & 0 deletions solana/programs/example-native-token-transfers/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub enum NTTError {
InvalidPendingTokenAuthority,
#[msg("IncorrectRentPayer")]
IncorrectRentPayer,
#[msg("InvalidMultisig")]
InvalidMultisig,
}

impl From<ScalingError> for NTTError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ use crate::{
bitmap::Bitmap,
error::NTTError,
queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState},
spl_multisig::SplMultisig,
};

#[derive(Accounts)]
#[instruction(args: InitializeArgs)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
Expand All @@ -37,12 +37,8 @@ pub struct Initialize<'info> {
)]
pub config: Box<Account<'info, crate::config::Config>>,

#[account(
constraint =
args.mode == Mode::Locking
|| mint.mint_authority.unwrap() == token_authority.key()
@ NTTError::InvalidMintAuthority,
)]
// NOTE: this account is unconstrained and is the responsibility of the
// handler to constrain it
pub mint: Box<InterfaceAccount<'info, token_interface::Mint>>,

#[account(
Expand Down Expand Up @@ -76,7 +72,7 @@ pub struct Initialize<'info> {
/// The custody account that holds tokens in locking mode and temporarily
/// holds tokens in burning mode.
/// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`]
/// function if the token account has already been created.
/// function if the token account has already been created.
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,

/// CHECK: checked to be the appropriate token program when initialising the
Expand All @@ -96,24 +92,77 @@ pub struct InitializeArgs {
}

pub fn initialize(ctx: Context<Initialize>, args: InitializeArgs) -> Result<()> {
ctx.accounts.config.set_inner(crate::config::Config {
bump: ctx.bumps.config,
mint: ctx.accounts.mint.key(),
token_program: ctx.accounts.token_program.key(),
mode: args.mode,
chain_id: ChainId { id: args.chain_id },
owner: ctx.accounts.deployer.key(),
// NOTE: this check was moved into the function body to reuse the `Initialize` struct
// in the multisig variant while preserving ABI
if args.mode == Mode::Burning
&& ctx.accounts.mint.mint_authority.unwrap() != ctx.accounts.token_authority.key()
{
return Err(NTTError::InvalidMintAuthority.into());
}

initialize_config_and_rate_limit(
ctx.accounts,
ctx.bumps.config,
args.chain_id,
args.limit,
args.mode,
)
}

#[derive(Accounts)]
#[instruction(args: InitializeArgs)]
pub struct InitializeMultisig<'info> {
#[account(
constraint =
args.mode == Mode::Locking
|| common.mint.mint_authority.unwrap() == multisig.key()
@ NTTError::InvalidMintAuthority,
)]
pub common: Initialize<'info>,

#[account(
constraint =
multisig.m == 1 && multisig.signers.contains(&common.token_authority.key())
@ NTTError::InvalidMultisig,
)]
pub multisig: InterfaceAccount<'info, SplMultisig>,
}

pub fn initialize_multisig(ctx: Context<InitializeMultisig>, args: InitializeArgs) -> Result<()> {
initialize_config_and_rate_limit(
&mut ctx.accounts.common,
ctx.bumps.common.config,
args.chain_id,
args.limit,
args.mode,
)
}

fn initialize_config_and_rate_limit(
common: &mut Initialize<'_>,
config_bump: u8,
chain_id: u16,
limit: u64,
mode: ntt_messages::mode::Mode,
) -> Result<()> {
common.config.set_inner(crate::config::Config {
bump: config_bump,
mint: common.mint.key(),
token_program: common.token_program.key(),
mode,
chain_id: ChainId { id: chain_id },
owner: common.deployer.key(),
pending_owner: None,
paused: false,
next_transceiver_id: 0,
// NOTE: can't be changed for now
threshold: 1,
enabled_transceivers: Bitmap::new(),
custody: ctx.accounts.custody.key(),
custody: common.custody.key(),
});

ctx.accounts.rate_limit.set_inner(OutboxRateLimit {
rate_limit: RateLimitState::new(args.limit),
common.rate_limit.set_inner(OutboxRateLimit {
rate_limit: RateLimitState::new(limit),
});

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
config::*,
error::NTTError,
queue::inbox::{InboxItem, ReleaseStatus},
spl_multisig::SplMultisig,
};

#[derive(Accounts)]
Expand Down Expand Up @@ -77,18 +78,11 @@ pub fn release_inbound_mint<'info>(
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
let inbox_item = &mut ctx.accounts.common.inbox_item;

let released = inbox_item.try_release()?;

if !released {
if args.revert_on_delay {
return Err(NTTError::CantReleaseYet.into());
} else {
return Ok(());
}
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
if inbox_item.is_none() {
return Ok(());
}

let inbox_item = inbox_item.unwrap();
assert!(inbox_item.release_status == ReleaseStatus::Released);

// NOTE: minting tokens is a two-step process:
Expand All @@ -106,6 +100,11 @@ pub fn release_inbound_mint<'info>(
// The [`transfer_burn`] function operates in a similar way
// (transfer to custody from sender, *then* burn).

let token_authority_sig: &[&[&[u8]]] = &[&[
crate::TOKEN_AUTHORITY_SEED,
&[ctx.bumps.common.token_authority],
]];

// Step 1: mint tokens to the custody account
token_interface::mint_to(
CpiContext::new_with_signer(
Expand All @@ -115,10 +114,7 @@ pub fn release_inbound_mint<'info>(
to: ctx.accounts.common.custody.to_account_info(),
authority: ctx.accounts.common.token_authority.to_account_info(),
},
&[&[
crate::TOKEN_AUTHORITY_SEED,
&[ctx.bumps.common.token_authority],
]],
token_authority_sig,
),
inbox_item.amount,
)?;
Expand All @@ -133,10 +129,87 @@ pub fn release_inbound_mint<'info>(
ctx.remaining_accounts,
inbox_item.amount,
ctx.accounts.common.mint.decimals,
&[&[
crate::TOKEN_AUTHORITY_SEED,
&[ctx.bumps.common.token_authority],
]],
token_authority_sig,
)?;
Ok(())
}

#[derive(Accounts)]
pub struct ReleaseInboundMintMultisig<'info> {
#[account(
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
)]
common: ReleaseInbound<'info>,

#[account(
constraint =
multisig.m == 1 && multisig.signers.contains(&common.token_authority.key())
@ NTTError::InvalidMultisig,
)]
pub multisig: InterfaceAccount<'info, SplMultisig>,
}

pub fn release_inbound_mint_multisig<'info>(
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
if inbox_item.is_none() {
return Ok(());
}
let inbox_item = inbox_item.unwrap();
assert!(inbox_item.release_status == ReleaseStatus::Released);

// NOTE: minting tokens is a two-step process:
// 1. Mint tokens to the custody account
// 2. Transfer the tokens from the custody account to the recipient
//
// This is done to ensure that if the token has a transfer hook defined, it
// will be called after the tokens are minted.
// Unfortunately the Token2022 program doesn't trigger transfer hooks when
// minting tokens, so we have to do it "manually" via a transfer.
//
// If we didn't do this, transfer hooks could be bypassed by transferring
// the tokens out through NTT first, then back in to the intended recipient.
//
// The [`transfer_burn`] function operates in a similar way
// (transfer to custody from sender, *then* burn).

let token_authority_sig: &[&[&[u8]]] = &[&[
crate::TOKEN_AUTHORITY_SEED,
&[ctx.bumps.common.token_authority],
]];

// Step 1: mint tokens to the custody account
solana_program::program::invoke_signed(
&spl_token_2022::instruction::mint_to(
&ctx.accounts.common.token_program.key(),
&ctx.accounts.common.mint.key(),
&ctx.accounts.common.custody.key(),
&ctx.accounts.multisig.key(),
&[&ctx.accounts.common.token_authority.key()],
inbox_item.amount,
)?,
&[
ctx.accounts.common.custody.to_account_info(),
ctx.accounts.common.mint.to_account_info(),
ctx.accounts.common.token_authority.to_account_info(),
ctx.accounts.multisig.to_account_info(),
],
token_authority_sig,
)?;

// Step 2: transfer the tokens from the custody account to the recipient
onchain::invoke_transfer_checked(
&ctx.accounts.common.token_program.key(),
ctx.accounts.common.custody.to_account_info(),
ctx.accounts.common.mint.to_account_info(),
ctx.accounts.common.recipient.to_account_info(),
ctx.accounts.common.token_authority.to_account_info(),
ctx.remaining_accounts,
inbox_item.amount,
ctx.accounts.common.mint.decimals,
token_authority_sig,
)?;
Ok(())
}
Expand All @@ -162,17 +235,12 @@ pub fn release_inbound_unlock<'info>(
ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
let inbox_item = &mut ctx.accounts.common.inbox_item;

let released = inbox_item.try_release()?;

if !released {
if args.revert_on_delay {
return Err(NTTError::CantReleaseYet.into());
} else {
return Ok(());
}
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
if inbox_item.is_none() {
return Ok(());
}
let inbox_item = inbox_item.unwrap();
assert!(inbox_item.release_status == ReleaseStatus::Released);

onchain::invoke_transfer_checked(
&ctx.accounts.common.token_program.key(),
Expand All @@ -190,3 +258,15 @@ pub fn release_inbound_unlock<'info>(
)?;
Ok(())
}
fn release_inbox_item(
inbox_item: &mut InboxItem,
revert_on_delay: bool,
) -> Result<Option<&mut InboxItem>> {
if inbox_item.try_release()? {
Ok(Some(inbox_item))
} else if revert_on_delay {
Err(NTTError::CantReleaseYet.into())
} else {
Ok(None)
}
}
15 changes: 15 additions & 0 deletions solana/programs/example-native-token-transfers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod peer;
pub mod pending_token_authority;
pub mod queue;
pub mod registered_transceiver;
pub mod spl_multisig;
pub mod transceivers;
pub mod transfer;

Expand Down Expand Up @@ -75,6 +76,13 @@ pub mod example_native_token_transfers {
instructions::initialize(ctx, args)
}

pub fn initialize_multisig(
ctx: Context<InitializeMultisig>,
args: InitializeArgs,
) -> Result<()> {
instructions::initialize_multisig(ctx, args)
}

pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
instructions::initialize_lut(ctx, recent_slot)
}
Expand Down Expand Up @@ -108,6 +116,13 @@ pub mod example_native_token_transfers {
instructions::release_inbound_mint(ctx, args)
}

pub fn release_inbound_mint_multisig<'info>(
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>,
args: ReleaseInboundArgs,
) -> Result<()> {
instructions::release_inbound_mint_multisig(ctx, args)
}

pub fn release_inbound_unlock<'info>(
ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>,
args: ReleaseInboundArgs,
Expand Down
35 changes: 35 additions & 0 deletions solana/programs/example-native-token-transfers/src/spl_multisig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use anchor_lang::{prelude::*, solana_program::program_pack::Pack, Ids, Owners};
use anchor_spl::token_interface::TokenInterface;
use std::ops::Deref;

/// Anchor does not have a SPL Multisig wrapper as a part of the token interface:
/// https://docs.rs/anchor-spl/0.29.0/src/anchor_spl/token_interface.rs.html
/// Thus, we have to write our own wrapper to use with `InterfaceAccount`
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SplMultisig(spl_token_2022::state::Multisig);

impl AccountDeserialize for SplMultisig {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
Ok(SplMultisig(spl_token_2022::state::Multisig::unpack(buf)?))
}
}

impl AccountSerialize for SplMultisig {}

impl Owners for SplMultisig {
fn owners() -> &'static [Pubkey] {
TokenInterface::ids()
}
}

impl Deref for SplMultisig {
type Target = spl_token_2022::state::Multisig;

fn deref(&self) -> &Self::Target {
&self.0
}
}

#[cfg(feature = "idl-build")]
impl anchor_lang::IdlBuild for SplMultisig {}
Loading

0 comments on commit 3311787

Please sign in to comment.