Skip to content

Commit b041c79

Browse files
committed
solana: Add multisig versions of initialize and release_inbound_mint
1 parent 738c67b commit b041c79

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::{associated_token::AssociatedToken, token_interface};
3+
use ntt_messages::{chain_id::ChainId, mode::Mode};
4+
use wormhole_solana_utils::cpi::bpf_loader_upgradeable::BpfLoaderUpgradeable;
5+
6+
#[cfg(feature = "idl-build")]
7+
use crate::messages::Hack;
8+
9+
use crate::{
10+
bitmap::Bitmap,
11+
error::NTTError,
12+
queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState},
13+
};
14+
15+
#[derive(Accounts)]
16+
#[instruction(args: InitializeMultisigArgs)]
17+
pub struct InitializeMultisig<'info> {
18+
#[account(mut)]
19+
pub payer: Signer<'info>,
20+
21+
#[account(address = program_data.upgrade_authority_address.unwrap_or_default())]
22+
pub deployer: Signer<'info>,
23+
24+
#[account(
25+
seeds = [crate::ID.as_ref()],
26+
bump,
27+
seeds::program = bpf_loader_upgradeable_program,
28+
)]
29+
program_data: Account<'info, ProgramData>,
30+
31+
#[account(
32+
init,
33+
space = 8 + crate::config::Config::INIT_SPACE,
34+
payer = payer,
35+
seeds = [crate::config::Config::SEED_PREFIX],
36+
bump
37+
)]
38+
pub config: Box<Account<'info, crate::config::Config>>,
39+
40+
#[account(
41+
constraint =
42+
args.mode == Mode::Locking
43+
|| mint.mint_authority.unwrap() == multisig.key()
44+
@ NTTError::InvalidMintAuthority,
45+
)]
46+
pub mint: Box<InterfaceAccount<'info, token_interface::Mint>>,
47+
48+
#[account(
49+
init,
50+
payer = payer,
51+
space = 8 + OutboxRateLimit::INIT_SPACE,
52+
seeds = [OutboxRateLimit::SEED_PREFIX],
53+
bump,
54+
)]
55+
pub rate_limit: Account<'info, OutboxRateLimit>,
56+
57+
#[account()]
58+
/// CHECK: multisig is mint authority
59+
pub multisig: UncheckedAccount<'info>,
60+
61+
#[account(
62+
seeds = [crate::TOKEN_AUTHORITY_SEED],
63+
bump,
64+
)]
65+
/// CHECK: [`token_authority`] is checked against the custody account and the [`mint`]'s mint_authority
66+
/// In any case, this function is used to set the Config and initialize the program so we
67+
/// assume the caller of this function will have total control over the program.
68+
///
69+
/// TODO: Using `UncheckedAccount` here leads to "Access violation in stack frame ...".
70+
/// Could refactor code to use `Box<_>` to reduce stack size.
71+
pub token_authority: AccountInfo<'info>,
72+
73+
#[account(
74+
init_if_needed,
75+
payer = payer,
76+
associated_token::mint = mint,
77+
associated_token::authority = token_authority,
78+
associated_token::token_program = token_program,
79+
)]
80+
/// The custody account that holds tokens in locking mode and temporarily
81+
/// holds tokens in burning mode.
82+
/// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`]
83+
/// function if the token account has already been created.
84+
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
85+
86+
/// CHECK: checked to be the appropriate token program when initialising the
87+
/// associated token account for the given mint.
88+
pub token_program: Interface<'info, token_interface::TokenInterface>,
89+
pub associated_token_program: Program<'info, AssociatedToken>,
90+
bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>,
91+
92+
system_program: Program<'info, System>,
93+
}
94+
95+
#[derive(AnchorSerialize, AnchorDeserialize)]
96+
pub struct InitializeMultisigArgs {
97+
pub chain_id: u16,
98+
pub limit: u64,
99+
pub mode: ntt_messages::mode::Mode,
100+
}
101+
102+
pub fn initialize_multisig(
103+
ctx: Context<InitializeMultisig>,
104+
args: InitializeMultisigArgs,
105+
) -> Result<()> {
106+
ctx.accounts.config.set_inner(crate::config::Config {
107+
bump: ctx.bumps.config,
108+
mint: ctx.accounts.mint.key(),
109+
token_program: ctx.accounts.token_program.key(),
110+
mode: args.mode,
111+
chain_id: ChainId { id: args.chain_id },
112+
owner: ctx.accounts.deployer.key(),
113+
pending_owner: None,
114+
paused: false,
115+
next_transceiver_id: 0,
116+
// NOTE: can't be changed for now
117+
threshold: 1,
118+
enabled_transceivers: Bitmap::new(),
119+
custody: ctx.accounts.custody.key(),
120+
});
121+
122+
ctx.accounts.rate_limit.set_inner(OutboxRateLimit {
123+
rate_limit: RateLimitState::new(args.limit),
124+
});
125+
126+
Ok(())
127+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
pub mod admin;
22
pub mod initialize;
3+
pub mod initialize_multisig;
34
pub mod luts;
45
pub mod mark_outbox_item_as_released;
56
pub mod redeem;
67
pub mod release_inbound;
8+
pub mod release_inbound_multisig;
79
pub mod transfer;
810

911
pub use admin::*;
1012
pub use initialize::*;
13+
pub use initialize_multisig::*;
1114
pub use luts::*;
1215
pub use mark_outbox_item_as_released::*;
1316
pub use redeem::*;
1417
pub use release_inbound::*;
18+
pub use release_inbound_multisig::*;
1519
pub use transfer::*;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token_interface;
3+
use ntt_messages::mode::Mode;
4+
use spl_token_2022::onchain;
5+
6+
use crate::{
7+
config::*,
8+
error::NTTError,
9+
queue::inbox::{InboxItem, ReleaseStatus},
10+
};
11+
12+
#[derive(Accounts)]
13+
pub struct ReleaseInboundMultisig<'info> {
14+
#[account(mut)]
15+
pub payer: Signer<'info>,
16+
17+
pub config: NotPausedConfig<'info>,
18+
19+
#[account(mut)]
20+
pub inbox_item: Account<'info, InboxItem>,
21+
22+
#[account(
23+
mut,
24+
associated_token::authority = inbox_item.recipient_address,
25+
associated_token::mint = mint,
26+
associated_token::token_program = token_program,
27+
)]
28+
pub recipient: InterfaceAccount<'info, token_interface::TokenAccount>,
29+
30+
/// CHECK: multisig account should be mint authority
31+
#[account(constraint = mint.mint_authority.unwrap() == multisig.key())]
32+
pub multisig: UncheckedAccount<'info>,
33+
34+
#[account(
35+
seeds = [crate::TOKEN_AUTHORITY_SEED],
36+
bump,
37+
)]
38+
/// CHECK The seeds constraint ensures that this is the correct address
39+
pub token_authority: UncheckedAccount<'info>,
40+
41+
#[account(
42+
mut,
43+
address = config.mint,
44+
)]
45+
/// CHECK: the mint address matches the config
46+
pub mint: InterfaceAccount<'info, token_interface::Mint>,
47+
48+
pub token_program: Interface<'info, token_interface::TokenInterface>,
49+
50+
/// CHECK: the token program checks if this indeed the right authority for the mint
51+
#[account(
52+
mut,
53+
address = config.custody
54+
)]
55+
pub custody: InterfaceAccount<'info, token_interface::TokenAccount>,
56+
}
57+
58+
#[derive(AnchorDeserialize, AnchorSerialize)]
59+
pub struct ReleaseInboundMultisigArgs {
60+
pub revert_on_delay: bool,
61+
}
62+
63+
// Burn/mint
64+
65+
#[derive(Accounts)]
66+
pub struct ReleaseInboundMultisigMint<'info> {
67+
#[account(
68+
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
69+
)]
70+
common: ReleaseInboundMultisig<'info>,
71+
}
72+
73+
/// Release an inbound transfer and mint the tokens to the recipient.
74+
/// When `revert_on_error` is true, the transaction will revert if the
75+
/// release timestamp has not been reached. When `revert_on_error` is false, the
76+
/// transaction succeeds, but the minting is not performed.
77+
/// Setting this flag to `false` is useful when bundling this instruction
78+
/// together with [`crate::instructions::redeem`] in a transaction, so that the minting
79+
/// is attempted optimistically.
80+
pub fn release_inbound_multisig_mint<'info>(
81+
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>,
82+
args: ReleaseInboundMultisigArgs,
83+
) -> Result<()> {
84+
let inbox_item = &mut ctx.accounts.common.inbox_item;
85+
86+
let released = inbox_item.try_release()?;
87+
88+
if !released {
89+
if args.revert_on_delay {
90+
return Err(NTTError::CantReleaseYet.into());
91+
} else {
92+
return Ok(());
93+
}
94+
}
95+
96+
assert!(inbox_item.release_status == ReleaseStatus::Released);
97+
98+
// NOTE: minting tokens is a two-step process:
99+
// 1. Mint tokens to the custody account
100+
// 2. Transfer the tokens from the custody account to the recipient
101+
//
102+
// This is done to ensure that if the token has a transfer hook defined, it
103+
// will be called after the tokens are minted.
104+
// Unfortunately the Token2022 program doesn't trigger transfer hooks when
105+
// minting tokens, so we have to do it "manually" via a transfer.
106+
//
107+
// If we didn't do this, transfer hooks could be bypassed by transferring
108+
// the tokens out through NTT first, then back in to the intended recipient.
109+
//
110+
// The [`transfer_burn`] function operates in a similar way
111+
// (transfer to custody from sender, *then* burn).
112+
113+
// Step 1: mint tokens to the custody account
114+
let ix = spl_token_2022::instruction::mint_to(
115+
&ctx.accounts.common.token_program.key(),
116+
&ctx.accounts.common.mint.key(),
117+
&ctx.accounts.common.custody.key(),
118+
&ctx.accounts.common.multisig.key(),
119+
&[&ctx.accounts.common.token_authority.key()],
120+
inbox_item.amount,
121+
)?;
122+
solana_program::program::invoke_signed(
123+
&ix,
124+
&[
125+
ctx.accounts.common.custody.to_account_info(),
126+
ctx.accounts.common.mint.to_account_info(),
127+
ctx.accounts.common.token_authority.to_account_info(),
128+
ctx.accounts.common.multisig.to_account_info(),
129+
],
130+
&[&[
131+
crate::TOKEN_AUTHORITY_SEED,
132+
&[ctx.bumps.common.token_authority],
133+
]],
134+
)?;
135+
136+
// Step 2: transfer the tokens from the custody account to the recipient
137+
onchain::invoke_transfer_checked(
138+
&ctx.accounts.common.token_program.key(),
139+
ctx.accounts.common.custody.to_account_info(),
140+
ctx.accounts.common.mint.to_account_info(),
141+
ctx.accounts.common.recipient.to_account_info(),
142+
ctx.accounts.common.token_authority.to_account_info(),
143+
ctx.remaining_accounts,
144+
inbox_item.amount,
145+
ctx.accounts.common.mint.decimals,
146+
&[&[
147+
crate::TOKEN_AUTHORITY_SEED,
148+
&[ctx.bumps.common.token_authority],
149+
]],
150+
)?;
151+
Ok(())
152+
}

solana/programs/example-native-token-transfers/src/lib.rs

+14
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ pub mod example_native_token_transfers {
7474
instructions::initialize(ctx, args)
7575
}
7676

77+
pub fn initialize_multisig(
78+
ctx: Context<InitializeMultisig>,
79+
args: InitializeMultisigArgs,
80+
) -> Result<()> {
81+
instructions::initialize_multisig(ctx, args)
82+
}
83+
7784
pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
7885
instructions::initialize_lut(ctx, recent_slot)
7986
}
@@ -114,6 +121,13 @@ pub mod example_native_token_transfers {
114121
instructions::release_inbound_unlock(ctx, args)
115122
}
116123

124+
pub fn release_inbound_multisig_mint<'info>(
125+
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>,
126+
args: ReleaseInboundMultisigArgs,
127+
) -> Result<()> {
128+
instructions::release_inbound_multisig_mint(ctx, args)
129+
}
130+
117131
pub fn transfer_ownership(ctx: Context<TransferOwnership>) -> Result<()> {
118132
instructions::transfer_ownership(ctx)
119133
}

0 commit comments

Comments
 (0)