Skip to content

Commit 5f2882e

Browse files
nvsriramjohnsaigle
andauthored
solana: Remove *_multisig instruction variants (wormhole-foundation#593)
* Replace `initialize_multisig` ix with optional multisig account in `Initialize` * Replace `release_inbound_mint_multisig` ix with optional multisig account in `ReleaseInboundMint` * Update cargo test to match new `Initialize` struct * Remove redundant `.to_account_info()` call * Update IDL * Update TS test case and SDK * Derive multisigTokenAuthority from token mint info * solana: Fix logic error in Inbox release process There are a few suspicious aspects of the inbox release logic: 1. `release_inbound_unlock` takes a flag that determines whether a `false` result from `try_release` should cause an error. The flag indicates that it should revert on delay. However, `try_release` can also return `false` when a transaction has not been approved. If the flag is set, the transaction will not return an error when a user tries to release a transaction that is not approved even though whether a transaction is approved has nothing to do with its expected behaviour when it is delayed 2. The custom error TransferNotApproved is defined but unused Taken together, I believe `try_release()` should revert with the TransferNotApproved error. This would disambiguate the various 'false' results and provide more intuitive behaviour when a user tries to release a transaction that is not approved. * solana: change comment to revert_on_delay * solana: Let redeem code return error instead of try_release * nit: Fix grammar * Update IDL * Update TS SDK to use renamed args * Update comment to match function logic --------- Co-authored-by: John Saigle <[email protected]>
1 parent b6b681a commit 5f2882e

File tree

12 files changed

+200
-836
lines changed

12 files changed

+200
-836
lines changed

solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,10 @@ fn claim_from_token_authority<'info>(
349349
) -> Result<()> {
350350
token_interface::set_authority(
351351
CpiContext::new_with_signer(
352-
token_program.to_account_info(),
352+
token_program,
353353
token_interface::SetAuthority {
354-
account_or_mint: mint.to_account_info(),
355-
current_authority: token_authority.to_account_info(),
354+
account_or_mint: mint,
355+
current_authority: token_authority,
356356
},
357357
&[&[crate::TOKEN_AUTHORITY_SEED, &[token_authority_bump]]],
358358
),

solana/programs/example-native-token-transfers/src/instructions/initialize.rs

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ use crate::messages::Hack;
88

99
use crate::{
1010
bitmap::Bitmap,
11+
config::Config,
1112
error::NTTError,
1213
queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState},
1314
spl_multisig::SplMultisig,
1415
};
1516

1617
#[derive(Accounts)]
18+
#[instruction(args: InitializeArgs)]
1719
pub struct Initialize<'info> {
1820
#[account(mut)]
1921
pub payer: Signer<'info>,
@@ -30,15 +32,20 @@ pub struct Initialize<'info> {
3032

3133
#[account(
3234
init,
33-
space = 8 + crate::config::Config::INIT_SPACE,
35+
space = 8 + Config::INIT_SPACE,
3436
payer = payer,
35-
seeds = [crate::config::Config::SEED_PREFIX],
37+
seeds = [Config::SEED_PREFIX],
3638
bump
3739
)]
38-
pub config: Box<Account<'info, crate::config::Config>>,
40+
pub config: Box<Account<'info, Config>>,
3941

40-
// NOTE: this account is unconstrained and is the responsibility of the
41-
// handler to constrain it
42+
#[account(
43+
constraint = args.mode == Mode::Locking
44+
|| mint.mint_authority.unwrap() == multisig_token_authority.as_ref().map_or(
45+
token_authority.key(),
46+
|multisig_token_authority| multisig_token_authority.key()
47+
) @ NTTError::InvalidMintAuthority
48+
)]
4249
pub mint: Box<InterfaceAccount<'info, token_interface::Mint>>,
4350

4451
#[account(
@@ -62,6 +69,13 @@ pub struct Initialize<'info> {
6269
/// Could refactor code to use `Box<_>` to reduce stack size.
6370
pub token_authority: AccountInfo<'info>,
6471

72+
#[account(
73+
constraint = multisig_token_authority.m == 1
74+
&& multisig_token_authority.signers.contains(&token_authority.key())
75+
@ NTTError::InvalidMultisig,
76+
)]
77+
pub multisig_token_authority: Option<Box<InterfaceAccount<'info, SplMultisig>>>,
78+
6579
#[account(
6680
init_if_needed,
6781
payer = payer,
@@ -92,14 +106,6 @@ pub struct InitializeArgs {
92106
}
93107

94108
pub fn initialize(ctx: Context<Initialize>, args: InitializeArgs) -> Result<()> {
95-
// NOTE: this check was moved into the function body to reuse the `Initialize` struct
96-
// in the multisig variant while preserving ABI
97-
if args.mode == Mode::Burning
98-
&& ctx.accounts.mint.mint_authority.unwrap() != ctx.accounts.token_authority.key()
99-
{
100-
return Err(NTTError::InvalidMintAuthority.into());
101-
}
102-
103109
initialize_config_and_rate_limit(
104110
ctx.accounts,
105111
ctx.bumps.config,
@@ -109,35 +115,6 @@ pub fn initialize(ctx: Context<Initialize>, args: InitializeArgs) -> Result<()>
109115
)
110116
}
111117

112-
#[derive(Accounts)]
113-
#[instruction(args: InitializeArgs)]
114-
pub struct InitializeMultisig<'info> {
115-
#[account(
116-
constraint =
117-
args.mode == Mode::Locking
118-
|| common.mint.mint_authority.unwrap() == multisig.key()
119-
@ NTTError::InvalidMintAuthority,
120-
)]
121-
pub common: Initialize<'info>,
122-
123-
#[account(
124-
constraint =
125-
multisig.m == 1 && multisig.signers.contains(&common.token_authority.key())
126-
@ NTTError::InvalidMultisig,
127-
)]
128-
pub multisig: InterfaceAccount<'info, SplMultisig>,
129-
}
130-
131-
pub fn initialize_multisig(ctx: Context<InitializeMultisig>, args: InitializeArgs) -> Result<()> {
132-
initialize_config_and_rate_limit(
133-
&mut ctx.accounts.common,
134-
ctx.bumps.common.config,
135-
args.chain_id,
136-
args.limit,
137-
args.mode,
138-
)
139-
}
140-
141118
fn initialize_config_and_rate_limit(
142119
common: &mut Initialize<'_>,
143120
config_bump: u8,

solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs

Lines changed: 84 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub struct ReleaseInbound<'info> {
5454

5555
#[derive(AnchorDeserialize, AnchorSerialize)]
5656
pub struct ReleaseInboundArgs {
57-
pub revert_on_delay: bool,
57+
pub revert_when_not_ready: bool,
5858
}
5959

6060
// Burn/mint
@@ -65,11 +65,18 @@ pub struct ReleaseInboundMint<'info> {
6565
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
6666
)]
6767
common: ReleaseInbound<'info>,
68+
69+
#[account(
70+
constraint = multisig_token_authority.m == 1
71+
&& multisig_token_authority.signers.contains(&common.token_authority.key())
72+
@ NTTError::InvalidMultisig,
73+
)]
74+
pub multisig_token_authority: Option<InterfaceAccount<'info, SplMultisig>>,
6875
}
6976

7077
/// Release an inbound transfer and mint the tokens to the recipient.
71-
/// When `revert_on_error` is true, the transaction will revert if the
72-
/// release timestamp has not been reached. When `revert_on_error` is false, the
78+
/// When `revert_when_not_ready` is true, the transaction will revert if the
79+
/// release timestamp has not been reached. When `revert_when_not_ready` is false, the
7380
/// transaction succeeds, but the minting is not performed.
7481
/// Setting this flag to `false` is useful when bundling this instruction
7582
/// together with [`crate::instructions::redeem`] in a transaction, so that the minting
@@ -78,7 +85,10 @@ pub fn release_inbound_mint<'info>(
7885
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>,
7986
args: ReleaseInboundArgs,
8087
) -> Result<()> {
81-
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
88+
let inbox_item = release_inbox_item(
89+
&mut ctx.accounts.common.inbox_item,
90+
args.revert_when_not_ready,
91+
)?;
8292
if inbox_item.is_none() {
8393
return Ok(());
8494
}
@@ -106,18 +116,25 @@ pub fn release_inbound_mint<'info>(
106116
]];
107117

108118
// Step 1: mint tokens to the custody account
109-
token_interface::mint_to(
110-
CpiContext::new_with_signer(
119+
match &ctx.accounts.multisig_token_authority {
120+
Some(multisig_token_authority) => mint_to_custody_from_multisig_token_authority(
111121
ctx.accounts.common.token_program.to_account_info(),
112-
token_interface::MintTo {
113-
mint: ctx.accounts.common.mint.to_account_info(),
114-
to: ctx.accounts.common.custody.to_account_info(),
115-
authority: ctx.accounts.common.token_authority.to_account_info(),
116-
},
122+
ctx.accounts.common.mint.to_account_info(),
123+
ctx.accounts.common.custody.to_account_info(),
124+
multisig_token_authority.to_account_info(),
125+
ctx.accounts.common.token_authority.to_account_info(),
117126
token_authority_sig,
118-
),
119-
inbox_item.amount,
120-
)?;
127+
inbox_item.amount,
128+
)?,
129+
None => mint_to_custody_from_token_authority(
130+
ctx.accounts.common.token_program.to_account_info(),
131+
ctx.accounts.common.mint.to_account_info(),
132+
ctx.accounts.common.custody.to_account_info(),
133+
ctx.accounts.common.token_authority.to_account_info(),
134+
token_authority_sig,
135+
inbox_item.amount,
136+
)?,
137+
};
121138

122139
// Step 2: transfer the tokens from the custody account to the recipient
123140
onchain::invoke_transfer_checked(
@@ -134,82 +151,49 @@ pub fn release_inbound_mint<'info>(
134151
Ok(())
135152
}
136153

137-
#[derive(Accounts)]
138-
pub struct ReleaseInboundMintMultisig<'info> {
139-
#[account(
140-
constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode,
141-
)]
142-
common: ReleaseInbound<'info>,
143-
144-
#[account(
145-
constraint =
146-
multisig.m == 1 && multisig.signers.contains(&common.token_authority.key())
147-
@ NTTError::InvalidMultisig,
148-
)]
149-
pub multisig: InterfaceAccount<'info, SplMultisig>,
154+
fn mint_to_custody_from_token_authority<'info>(
155+
token_program: AccountInfo<'info>,
156+
mint: AccountInfo<'info>,
157+
custody: AccountInfo<'info>,
158+
token_authority: AccountInfo<'info>,
159+
token_authority_signer_seeds: &[&[&[u8]]],
160+
amount: u64,
161+
) -> Result<()> {
162+
token_interface::mint_to(
163+
CpiContext::new_with_signer(
164+
token_program,
165+
token_interface::MintTo {
166+
mint,
167+
to: custody,
168+
authority: token_authority,
169+
},
170+
token_authority_signer_seeds,
171+
),
172+
amount,
173+
)?;
174+
Ok(())
150175
}
151176

152-
pub fn release_inbound_mint_multisig<'info>(
153-
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>,
154-
args: ReleaseInboundArgs,
177+
fn mint_to_custody_from_multisig_token_authority<'info>(
178+
token_program: AccountInfo<'info>,
179+
mint: AccountInfo<'info>,
180+
custody: AccountInfo<'info>,
181+
multisig_token_authority: AccountInfo<'info>,
182+
token_authority: AccountInfo<'info>,
183+
token_authority_signer_seeds: &[&[&[u8]]],
184+
amount: u64,
155185
) -> Result<()> {
156-
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
157-
if inbox_item.is_none() {
158-
return Ok(());
159-
}
160-
let inbox_item = inbox_item.unwrap();
161-
assert!(inbox_item.release_status == ReleaseStatus::Released);
162-
163-
// NOTE: minting tokens is a two-step process:
164-
// 1. Mint tokens to the custody account
165-
// 2. Transfer the tokens from the custody account to the recipient
166-
//
167-
// This is done to ensure that if the token has a transfer hook defined, it
168-
// will be called after the tokens are minted.
169-
// Unfortunately the Token2022 program doesn't trigger transfer hooks when
170-
// minting tokens, so we have to do it "manually" via a transfer.
171-
//
172-
// If we didn't do this, transfer hooks could be bypassed by transferring
173-
// the tokens out through NTT first, then back in to the intended recipient.
174-
//
175-
// The [`transfer_burn`] function operates in a similar way
176-
// (transfer to custody from sender, *then* burn).
177-
178-
let token_authority_sig: &[&[&[u8]]] = &[&[
179-
crate::TOKEN_AUTHORITY_SEED,
180-
&[ctx.bumps.common.token_authority],
181-
]];
182-
183-
// Step 1: mint tokens to the custody account
184186
solana_program::program::invoke_signed(
185187
&spl_token_2022::instruction::mint_to(
186-
&ctx.accounts.common.token_program.key(),
187-
&ctx.accounts.common.mint.key(),
188-
&ctx.accounts.common.custody.key(),
189-
&ctx.accounts.multisig.key(),
190-
&[&ctx.accounts.common.token_authority.key()],
191-
inbox_item.amount,
188+
&token_program.key(),
189+
&mint.key(),
190+
&custody.key(),
191+
&multisig_token_authority.key(),
192+
&[&token_authority.key()],
193+
amount,
192194
)?,
193-
&[
194-
ctx.accounts.common.custody.to_account_info(),
195-
ctx.accounts.common.mint.to_account_info(),
196-
ctx.accounts.common.token_authority.to_account_info(),
197-
ctx.accounts.multisig.to_account_info(),
198-
],
199-
token_authority_sig,
200-
)?;
201-
202-
// Step 2: transfer the tokens from the custody account to the recipient
203-
onchain::invoke_transfer_checked(
204-
&ctx.accounts.common.token_program.key(),
205-
ctx.accounts.common.custody.to_account_info(),
206-
ctx.accounts.common.mint.to_account_info(),
207-
ctx.accounts.common.recipient.to_account_info(),
208-
ctx.accounts.common.token_authority.to_account_info(),
209-
ctx.remaining_accounts,
210-
inbox_item.amount,
211-
ctx.accounts.common.mint.decimals,
212-
token_authority_sig,
195+
&[custody, mint, token_authority, multisig_token_authority],
196+
token_authority_signer_seeds,
213197
)?;
214198
Ok(())
215199
}
@@ -225,8 +209,8 @@ pub struct ReleaseInboundUnlock<'info> {
225209
}
226210

227211
/// Release an inbound transfer and unlock the tokens to the recipient.
228-
/// When `revert_on_error` is true, the transaction will revert if the
229-
/// release timestamp has not been reached. When `revert_on_error` is false, the
212+
/// When `revert_when_not_ready` is true, the transaction will revert if the
213+
/// release timestamp has not been reached. When `revert_when_not_ready` is false, the
230214
/// transaction succeeds, but the unlocking is not performed.
231215
/// Setting this flag to `false` is useful when bundling this instruction
232216
/// together with [`crate::instructions::redeem`], so that the unlocking
@@ -235,7 +219,10 @@ pub fn release_inbound_unlock<'info>(
235219
ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>,
236220
args: ReleaseInboundArgs,
237221
) -> Result<()> {
238-
let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?;
222+
let inbox_item = release_inbox_item(
223+
&mut ctx.accounts.common.inbox_item,
224+
args.revert_when_not_ready,
225+
)?;
239226
if inbox_item.is_none() {
240227
return Ok(());
241228
}
@@ -258,14 +245,21 @@ pub fn release_inbound_unlock<'info>(
258245
)?;
259246
Ok(())
260247
}
248+
261249
fn release_inbox_item(
262250
inbox_item: &mut InboxItem,
263-
revert_on_delay: bool,
251+
revert_when_not_ready: bool,
264252
) -> Result<Option<&mut InboxItem>> {
265253
if inbox_item.try_release()? {
266254
Ok(Some(inbox_item))
267-
} else if revert_on_delay {
268-
Err(NTTError::CantReleaseYet.into())
255+
} else if revert_when_not_ready {
256+
match inbox_item.release_status {
257+
ReleaseStatus::NotApproved => Err(NTTError::TransferNotApproved.into()),
258+
ReleaseStatus::ReleaseAfter(_) => Err(NTTError::CantReleaseYet.into()),
259+
// Unreachable: if released, [`InboxItem::try_release`] will return an Error immediately
260+
// rather than Ok(bool).
261+
ReleaseStatus::Released => Err(NTTError::TransferAlreadyRedeemed.into()),
262+
}
269263
} else {
270264
Ok(None)
271265
}

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,6 @@ pub mod example_native_token_transfers {
7676
instructions::initialize(ctx, args)
7777
}
7878

79-
pub fn initialize_multisig(
80-
ctx: Context<InitializeMultisig>,
81-
args: InitializeArgs,
82-
) -> Result<()> {
83-
instructions::initialize_multisig(ctx, args)
84-
}
85-
8679
pub fn initialize_lut(ctx: Context<InitializeLUT>, recent_slot: u64) -> Result<()> {
8780
instructions::initialize_lut(ctx, recent_slot)
8881
}
@@ -116,13 +109,6 @@ pub mod example_native_token_transfers {
116109
instructions::release_inbound_mint(ctx, args)
117110
}
118111

119-
pub fn release_inbound_mint_multisig<'info>(
120-
ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>,
121-
args: ReleaseInboundArgs,
122-
) -> Result<()> {
123-
instructions::release_inbound_mint_multisig(ctx, args)
124-
}
125-
126112
pub fn release_inbound_unlock<'info>(
127113
ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>,
128114
args: ReleaseInboundArgs,

0 commit comments

Comments
 (0)