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: Add SPL multisig support #568

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
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 @@ -53,6 +53,8 @@ pub enum NTTError {
BitmapIndexOutOfBounds,
#[msg("NoRegisteredTransceivers")]
NoRegisteredTransceivers,
#[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,6 @@ 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,
)]
pub mint: Box<InterfaceAccount<'info, token_interface::Mint>>,
nvsriram marked this conversation as resolved.
Show resolved Hide resolved

#[account(
Expand Down Expand Up @@ -76,7 +70,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 +90,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::Locking
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
|| ctx.accounts.mint.mint_authority.unwrap() == ctx.accounts.token_authority.key())
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
{
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 @@ -62,6 +63,7 @@ pub struct ReleaseInboundArgs {
pub struct ReleaseInboundMint<'info> {
#[account(
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
constraint = common.mint.mint_authority.unwrap() == common.token_authority.key()
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
)]
common: ReleaseInbound<'info>,
}
Expand All @@ -77,19 +79,7 @@ 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(());
}
}

assert!(inbox_item.release_status == ReleaseStatus::Released);
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
nvsriram marked this conversation as resolved.
Show resolved Hide resolved

// NOTE: minting tokens is a two-step process:
// 1. Mint tokens to the custody account
Expand All @@ -106,6 +96,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 +110,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 +125,83 @@ 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,
constraint = common.mint.mint_authority.unwrap() == multisig.key()
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
)]
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)?;
nvsriram marked this conversation as resolved.
Show resolved Hide resolved

// 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 @@ -147,6 +212,7 @@ pub fn release_inbound_mint<'info>(
pub struct ReleaseInboundUnlock<'info> {
#[account(
constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode,
constraint = common.mint.mint_authority.unwrap() == common.token_authority.key()
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
)]
common: ReleaseInbound<'info>,
}
Expand All @@ -162,17 +228,7 @@ 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)?;

onchain::invoke_transfer_checked(
&ctx.accounts.common.token_program.key(),
Expand All @@ -190,3 +246,14 @@ pub fn release_inbound_unlock<'info>(
)?;
Ok(())
}

fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Result<&mut InboxItem> {
if inbox_item.try_release()? {
assert!(inbox_item.release_status == ReleaseStatus::Released);
Ok(inbox_item)
} else if revert_on_delay {
Err(NTTError::CantReleaseYet.into())
} else {
Ok(inbox_item)
}
}
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 @@ -20,6 +20,7 @@ pub mod messages;
pub mod peer;
pub mod queue;
pub mod registered_transceiver;
pub mod spl_multisig;
pub mod transceivers;
pub mod transfer;

Expand Down Expand Up @@ -74,6 +75,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 @@ -107,6 +115,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
37 changes: 37 additions & 0 deletions solana/programs/example-native-token-transfers/src/spl_multisig.rs
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use anchor_lang::{prelude::*, solana_program::program_pack::Pack, Ids, Owners};
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
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> {
spl_token_2022::state::Multisig::unpack(buf)
.map(SplMultisig)
.map_err(Into::into)
nvsriram marked this conversation as resolved.
Show resolved Hide resolved
}
}

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
Loading