diff --git a/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs index c809331d5..070390bbe 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/admin/transfer_token_authority.rs @@ -349,10 +349,10 @@ fn claim_from_token_authority<'info>( ) -> Result<()> { token_interface::set_authority( CpiContext::new_with_signer( - token_program.to_account_info(), + token_program, token_interface::SetAuthority { - account_or_mint: mint.to_account_info(), - current_authority: token_authority.to_account_info(), + account_or_mint: mint, + current_authority: token_authority, }, &[&[crate::TOKEN_AUTHORITY_SEED, &[token_authority_bump]]], ), diff --git a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs index f87896e3d..ccd470e33 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -8,12 +8,14 @@ use crate::messages::Hack; use crate::{ bitmap::Bitmap, + config::Config, 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>, @@ -30,15 +32,20 @@ pub struct Initialize<'info> { #[account( init, - space = 8 + crate::config::Config::INIT_SPACE, + space = 8 + Config::INIT_SPACE, payer = payer, - seeds = [crate::config::Config::SEED_PREFIX], + seeds = [Config::SEED_PREFIX], bump )] - pub config: Box>, + pub config: Box>, - // NOTE: this account is unconstrained and is the responsibility of the - // handler to constrain it + #[account( + constraint = args.mode == Mode::Locking + || mint.mint_authority.unwrap() == multisig_token_authority.as_ref().map_or( + token_authority.key(), + |multisig_token_authority| multisig_token_authority.key() + ) @ NTTError::InvalidMintAuthority + )] pub mint: Box>, #[account( @@ -62,6 +69,13 @@ pub struct Initialize<'info> { /// Could refactor code to use `Box<_>` to reduce stack size. pub token_authority: AccountInfo<'info>, + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>>, + #[account( init_if_needed, payer = payer, @@ -92,14 +106,6 @@ pub struct InitializeArgs { } pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { - // 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, @@ -109,35 +115,6 @@ pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> ) } -#[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, 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, diff --git a/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs b/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs index 623c3b509..5047b40af 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/release_inbound.rs @@ -54,7 +54,7 @@ pub struct ReleaseInbound<'info> { #[derive(AnchorDeserialize, AnchorSerialize)] pub struct ReleaseInboundArgs { - pub revert_on_delay: bool, + pub revert_when_not_ready: bool, } // Burn/mint @@ -65,11 +65,18 @@ pub struct ReleaseInboundMint<'info> { constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, )] common: ReleaseInbound<'info>, + + #[account( + constraint = multisig_token_authority.m == 1 + && multisig_token_authority.signers.contains(&common.token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig_token_authority: Option>, } /// Release an inbound transfer and mint the tokens to the recipient. -/// When `revert_on_error` is true, the transaction will revert if the -/// release timestamp has not been reached. When `revert_on_error` is false, the +/// When `revert_when_not_ready` is true, the transaction will revert if the +/// release timestamp has not been reached. When `revert_when_not_ready` is false, the /// transaction succeeds, but the minting is not performed. /// Setting this flag to `false` is useful when bundling this instruction /// together with [`crate::instructions::redeem`] in a transaction, so that the minting @@ -78,7 +85,10 @@ pub fn release_inbound_mint<'info>( ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { - let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?; + let inbox_item = release_inbox_item( + &mut ctx.accounts.common.inbox_item, + args.revert_when_not_ready, + )?; if inbox_item.is_none() { return Ok(()); } @@ -106,18 +116,25 @@ pub fn release_inbound_mint<'info>( ]]; // Step 1: mint tokens to the custody account - token_interface::mint_to( - CpiContext::new_with_signer( + match &ctx.accounts.multisig_token_authority { + Some(multisig_token_authority) => mint_to_custody_from_multisig_token_authority( ctx.accounts.common.token_program.to_account_info(), - token_interface::MintTo { - mint: ctx.accounts.common.mint.to_account_info(), - to: ctx.accounts.common.custody.to_account_info(), - authority: ctx.accounts.common.token_authority.to_account_info(), - }, + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.custody.to_account_info(), + multisig_token_authority.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), token_authority_sig, - ), - inbox_item.amount, - )?; + inbox_item.amount, + )?, + None => mint_to_custody_from_token_authority( + ctx.accounts.common.token_program.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.custody.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + token_authority_sig, + inbox_item.amount, + )?, + }; // Step 2: transfer the tokens from the custody account to the recipient onchain::invoke_transfer_checked( @@ -134,82 +151,49 @@ pub fn release_inbound_mint<'info>( 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>, +fn mint_to_custody_from_token_authority<'info>( + token_program: AccountInfo<'info>, + mint: AccountInfo<'info>, + custody: AccountInfo<'info>, + token_authority: AccountInfo<'info>, + token_authority_signer_seeds: &[&[&[u8]]], + amount: u64, +) -> Result<()> { + token_interface::mint_to( + CpiContext::new_with_signer( + token_program, + token_interface::MintTo { + mint, + to: custody, + authority: token_authority, + }, + token_authority_signer_seeds, + ), + amount, + )?; + Ok(()) } -pub fn release_inbound_mint_multisig<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>, - args: ReleaseInboundArgs, +fn mint_to_custody_from_multisig_token_authority<'info>( + token_program: AccountInfo<'info>, + mint: AccountInfo<'info>, + custody: AccountInfo<'info>, + multisig_token_authority: AccountInfo<'info>, + token_authority: AccountInfo<'info>, + token_authority_signer_seeds: &[&[&[u8]]], + amount: u64, ) -> 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, + &token_program.key(), + &mint.key(), + &custody.key(), + &multisig_token_authority.key(), + &[&token_authority.key()], + 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, + &[custody, mint, token_authority, multisig_token_authority], + token_authority_signer_seeds, )?; Ok(()) } @@ -225,8 +209,8 @@ pub struct ReleaseInboundUnlock<'info> { } /// Release an inbound transfer and unlock the tokens to the recipient. -/// When `revert_on_error` is true, the transaction will revert if the -/// release timestamp has not been reached. When `revert_on_error` is false, the +/// When `revert_when_not_ready` is true, the transaction will revert if the +/// release timestamp has not been reached. When `revert_when_not_ready` is false, the /// transaction succeeds, but the unlocking is not performed. /// Setting this flag to `false` is useful when bundling this instruction /// together with [`crate::instructions::redeem`], so that the unlocking @@ -235,7 +219,10 @@ pub fn release_inbound_unlock<'info>( ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { - let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?; + let inbox_item = release_inbox_item( + &mut ctx.accounts.common.inbox_item, + args.revert_when_not_ready, + )?; if inbox_item.is_none() { return Ok(()); } @@ -258,14 +245,21 @@ pub fn release_inbound_unlock<'info>( )?; Ok(()) } + fn release_inbox_item( inbox_item: &mut InboxItem, - revert_on_delay: bool, + revert_when_not_ready: bool, ) -> Result> { if inbox_item.try_release()? { Ok(Some(inbox_item)) - } else if revert_on_delay { - Err(NTTError::CantReleaseYet.into()) + } else if revert_when_not_ready { + match inbox_item.release_status { + ReleaseStatus::NotApproved => Err(NTTError::TransferNotApproved.into()), + ReleaseStatus::ReleaseAfter(_) => Err(NTTError::CantReleaseYet.into()), + // Unreachable: if released, [`InboxItem::try_release`] will return an Error immediately + // rather than Ok(bool). + ReleaseStatus::Released => Err(NTTError::TransferAlreadyRedeemed.into()), + } } else { Ok(None) } diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index c581a35dd..49d6d802f 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -76,13 +76,6 @@ pub mod example_native_token_transfers { instructions::initialize(ctx, args) } - pub fn initialize_multisig( - ctx: Context, - args: InitializeArgs, - ) -> Result<()> { - instructions::initialize_multisig(ctx, args) - } - pub fn initialize_lut(ctx: Context, recent_slot: u64) -> Result<()> { instructions::initialize_lut(ctx, recent_slot) } @@ -116,13 +109,6 @@ 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, diff --git a/solana/programs/example-native-token-transfers/src/queue/inbox.rs b/solana/programs/example-native-token-transfers/src/queue/inbox.rs index e38546c32..396ed6221 100644 --- a/solana/programs/example-native-token-transfers/src/queue/inbox.rs +++ b/solana/programs/example-native-token-transfers/src/queue/inbox.rs @@ -35,7 +35,16 @@ impl InboxItem { pub const SEED_PREFIX: &'static [u8] = b"inbox_item"; /// Attempt to release the transfer. - /// Returns true if the transfer was released, false if it was not yet time to release it. + /// + /// * If the inbox item status is [`ReleaseStatus::ReleaseAfter`], this function returns true if the current timestamp + /// is newer than the one stored in the release status. If the timestamp is in the future, returns false. + /// * If the inbox item status is [`ReleaseStatus::NotApproved`], this function returns false. + /// + /// # Errors + /// + /// Returns [`NTTError::TransferAlreadyRedeemed`] if the inbox item status is [`ReleaseStatus::Released`]. + /// This is important to prevent a single transfer from being redeemed multiple times, which would + /// result in minting arbitrary amounts of the token. pub fn try_release(&mut self) -> Result { let now = current_timestamp(); diff --git a/solana/programs/example-native-token-transfers/tests/common/setup.rs b/solana/programs/example-native-token-transfers/tests/common/setup.rs index 500f6b66d..ce7b554c3 100644 --- a/solana/programs/example-native-token-transfers/tests/common/setup.rs +++ b/solana/programs/example-native-token-transfers/tests/common/setup.rs @@ -179,6 +179,7 @@ pub async fn setup_ntt_with_token_program_id( payer: ctx.payer.pubkey(), deployer: test_data.program_owner.pubkey(), mint: test_data.mint, + multisig_token_authority: None, }, InitializeArgs { // TODO: use sdk diff --git a/solana/programs/example-native-token-transfers/tests/sdk/instructions/initialize.rs b/solana/programs/example-native-token-transfers/tests/sdk/instructions/initialize.rs index ea6a7e02f..a771690d0 100644 --- a/solana/programs/example-native-token-transfers/tests/sdk/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/tests/sdk/instructions/initialize.rs @@ -10,6 +10,7 @@ pub struct Initialize { pub payer: Pubkey, pub deployer: Pubkey, pub mint: Pubkey, + pub multisig_token_authority: Option, } pub fn initialize(ntt: &NTT, accounts: Initialize, args: InitializeArgs) -> Instruction { @@ -33,6 +34,7 @@ pub fn initialize_with_token_program_id( mint: accounts.mint, rate_limit: ntt.outbox_rate_limit(), token_authority: ntt.token_authority(), + multisig_token_authority: accounts.multisig_token_authority, custody: ntt.custody_with_token_program_id(&accounts.mint, token_program_id), token_program: *token_program_id, associated_token_program: AssociatedToken::id(), diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index 00004c973..9f9cb49e1 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -176,7 +176,7 @@ describe("example-native-token-transfers", () => { mint: testMint.address, outboundLimit: 1_000_000n, mode: "burning", - multisig: multisigTokenAuthority, + multisigTokenAuthority, }); await signSendWait(ctx, initTxs, signer); @@ -504,7 +504,7 @@ describe("example-native-token-transfers", () => { const published = emitter.publishMessage(0, serialized, 200); const rawVaa = guardians.addSignatures(published, [0]); const vaa = deserialize("Ntt:WormholeTransfer", serialize(rawVaa)); - const redeemTxs = ntt.redeem([vaa], sender, multisigTokenAuthority); + const redeemTxs = ntt.redeem([vaa], sender); await signSendWait(ctx, redeemTxs, signer); assert.bn(await testDummyTransferHook.counter.value()).equal(2); diff --git a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json index 5c81a0e1a..661292f14 100644 --- a/solana/ts/idl/3_0_0/json/example_native_token_transfers.json +++ b/solana/ts/idl/3_0_0/json/example_native_token_transfers.json @@ -47,6 +47,12 @@ "Could refactor code to use `Box<_>` to reduce stack size." ] }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "custody", "isMut": true, @@ -90,104 +96,6 @@ } ] }, - { - "name": "initializeMultisig", - "accounts": [ - { - "name": "common", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "deployer", - "isMut": false, - "isSigner": true - }, - { - "name": "programData", - "isMut": false, - "isSigner": false - }, - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rateLimit", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "In any case, this function is used to set the Config and initialize the program so we", - "assume the caller of this function will have total control over the program.", - "", - "TODO: Using `UncheckedAccount` here leads to \"Access violation in stack frame ...\".", - "Could refactor code to use `Box<_>` to reduce stack size." - ] - }, - { - "name": "custody", - "isMut": true, - "isSigner": false, - "docs": [ - "The custody account that holds tokens in locking mode and temporarily", - "holds tokens in burning mode.", - "function if the token account has already been created." - ] - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "associated token account for the given mint." - ] - }, - { - "name": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "bpfLoaderUpgradeableProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ] - }, - { - "name": "multisig", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "InitializeArgs" - } - } - ] - }, { "name": "initializeLut", "accounts": [ @@ -584,72 +492,6 @@ }, { "name": "releaseInboundMint", - "accounts": [ - { - "name": "common", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "config", - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - } - ] - }, - { - "name": "inboxItem", - "isMut": true, - "isSigner": false - }, - { - "name": "recipient", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK The seeds constraint ensures that this is the correct address" - ] - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "custody", - "isMut": true, - "isSigner": false - } - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ReleaseInboundArgs" - } - } - ] - }, - { - "name": "releaseInboundMintMultisig", "accounts": [ { "name": "common", @@ -705,9 +547,10 @@ ] }, { - "name": "multisig", + "name": "multisigTokenAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "isOptional": true } ], "args": [ @@ -2286,7 +2129,7 @@ "kind": "struct", "fields": [ { - "name": "revertOnDelay", + "name": "revertWhenNotReady", "type": "bool" } ] diff --git a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts index f49692f66..cb04c8080 100644 --- a/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts +++ b/solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts @@ -47,6 +47,12 @@ export type ExampleNativeTokenTransfers = { "Could refactor code to use `Box<_>` to reduce stack size." ] }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "custody", "isMut": true, @@ -90,104 +96,6 @@ export type ExampleNativeTokenTransfers = { } ] }, - { - "name": "initializeMultisig", - "accounts": [ - { - "name": "common", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "deployer", - "isMut": false, - "isSigner": true - }, - { - "name": "programData", - "isMut": false, - "isSigner": false - }, - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rateLimit", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "In any case, this function is used to set the Config and initialize the program so we", - "assume the caller of this function will have total control over the program.", - "", - "TODO: Using `UncheckedAccount` here leads to \"Access violation in stack frame ...\".", - "Could refactor code to use `Box<_>` to reduce stack size." - ] - }, - { - "name": "custody", - "isMut": true, - "isSigner": false, - "docs": [ - "The custody account that holds tokens in locking mode and temporarily", - "holds tokens in burning mode.", - "function if the token account has already been created." - ] - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "associated token account for the given mint." - ] - }, - { - "name": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "bpfLoaderUpgradeableProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ] - }, - { - "name": "multisig", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "InitializeArgs" - } - } - ] - }, { "name": "initializeLut", "accounts": [ @@ -584,72 +492,6 @@ export type ExampleNativeTokenTransfers = { }, { "name": "releaseInboundMint", - "accounts": [ - { - "name": "common", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "config", - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - } - ] - }, - { - "name": "inboxItem", - "isMut": true, - "isSigner": false - }, - { - "name": "recipient", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK The seeds constraint ensures that this is the correct address" - ] - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "custody", - "isMut": true, - "isSigner": false - } - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ReleaseInboundArgs" - } - } - ] - }, - { - "name": "releaseInboundMintMultisig", "accounts": [ { "name": "common", @@ -705,9 +547,10 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "multisig", + "name": "multisigTokenAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "isOptional": true } ], "args": [ @@ -2286,7 +2129,7 @@ export type ExampleNativeTokenTransfers = { "kind": "struct", "fields": [ { - "name": "revertOnDelay", + "name": "revertWhenNotReady", "type": "bool" } ] @@ -2699,6 +2542,12 @@ export const IDL: ExampleNativeTokenTransfers = { "Could refactor code to use `Box<_>` to reduce stack size." ] }, + { + "name": "multisigTokenAuthority", + "isMut": false, + "isSigner": false, + "isOptional": true + }, { "name": "custody", "isMut": true, @@ -2742,104 +2591,6 @@ export const IDL: ExampleNativeTokenTransfers = { } ] }, - { - "name": "initializeMultisig", - "accounts": [ - { - "name": "common", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "deployer", - "isMut": false, - "isSigner": true - }, - { - "name": "programData", - "isMut": false, - "isSigner": false - }, - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "rateLimit", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "In any case, this function is used to set the Config and initialize the program so we", - "assume the caller of this function will have total control over the program.", - "", - "TODO: Using `UncheckedAccount` here leads to \"Access violation in stack frame ...\".", - "Could refactor code to use `Box<_>` to reduce stack size." - ] - }, - { - "name": "custody", - "isMut": true, - "isSigner": false, - "docs": [ - "The custody account that holds tokens in locking mode and temporarily", - "holds tokens in burning mode.", - "function if the token account has already been created." - ] - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "associated token account for the given mint." - ] - }, - { - "name": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "bpfLoaderUpgradeableProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ] - }, - { - "name": "multisig", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "InitializeArgs" - } - } - ] - }, { "name": "initializeLut", "accounts": [ @@ -3236,72 +2987,6 @@ export const IDL: ExampleNativeTokenTransfers = { }, { "name": "releaseInboundMint", - "accounts": [ - { - "name": "common", - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "config", - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - } - ] - }, - { - "name": "inboxItem", - "isMut": true, - "isSigner": false - }, - { - "name": "recipient", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenAuthority", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK The seeds constraint ensures that this is the correct address" - ] - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "custody", - "isMut": true, - "isSigner": false - } - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ReleaseInboundArgs" - } - } - ] - }, - { - "name": "releaseInboundMintMultisig", "accounts": [ { "name": "common", @@ -3357,9 +3042,10 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "multisig", + "name": "multisigTokenAuthority", "isMut": false, - "isSigner": false + "isSigner": false, + "isOptional": true } ], "args": [ @@ -4938,7 +4624,7 @@ export const IDL: ExampleNativeTokenTransfers = { "kind": "struct", "fields": [ { - "name": "revertOnDelay", + "name": "revertWhenNotReady", "type": "bool" } ] diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 67ec69bff..eff78a805 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -244,9 +244,11 @@ export namespace NTT { outboundLimit: bigint; tokenProgram: PublicKey; mode: "burning" | "locking"; + multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ) { + const [major, , ,] = parseVersion(program.idl.version); const mode: any = args.mode === "burning" ? { burning: {} } : { locking: {} }; const chainId = toChainId(args.chain); @@ -256,7 +258,7 @@ export namespace NTT { const limit = new BN(args.outboundLimit.toString()); return program.methods .initialize({ chainId, limit: limit, mode }) - .accountsStrict({ + .accounts({ payer: args.payer, deployer: args.owner, programData: programDataAddress(program.programId), @@ -265,6 +267,10 @@ export namespace NTT { rateLimit: pdas.outboxRateLimitAccount(), tokenProgram: args.tokenProgram, tokenAuthority: pdas.tokenAuthority(), + // NOTE: SPL Multisig token authority is only supported for versions >= 3.x.x + ...(major >= 3 && { + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }), custody: await NTT.custodyAccountAddress( pdas, args.mint, @@ -277,53 +283,6 @@ export namespace NTT { .instruction(); } - export async function createInitializeMultisigInstruction( - program: Program>, - args: { - payer: PublicKey; - owner: PublicKey; - chain: Chain; - mint: PublicKey; - outboundLimit: bigint; - tokenProgram: PublicKey; - mode: "burning" | "locking"; - multisig: PublicKey; - }, - pdas?: Pdas - ) { - const mode: any = - args.mode === "burning" ? { burning: {} } : { locking: {} }; - const chainId = toChainId(args.chain); - - pdas = pdas ?? NTT.pdas(program.programId); - - const limit = new BN(args.outboundLimit.toString()); - return await program.methods - .initializeMultisig({ chainId, limit: limit, mode }) - .accountsStrict({ - common: { - payer: args.payer, - deployer: args.owner, - programData: programDataAddress(program.programId), - config: pdas.configAccount(), - mint: args.mint, - rateLimit: pdas.outboxRateLimitAccount(), - tokenAuthority: pdas.tokenAuthority(), - custody: await NTT.custodyAccountAddress( - pdas, - args.mint, - args.tokenProgram - ), - tokenProgram: args.tokenProgram, - associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_PROGRAM_ID, - systemProgram: SystemProgram.programId, - }, - multisig: args.multisig, - }) - .instruction(); - } - // This function should be called after each upgrade. If there's nothing to // do, it won't actually submit a transaction, so it's cheap to call. export async function initializeOrUpdateLUT( @@ -588,40 +547,14 @@ export namespace NTT { payer: PublicKey; chain: Chain; nttMessage: Ntt.Message; - revertOnDelay: boolean; + revertWhenNotReady: boolean; recipient?: PublicKey; }, pdas?: Pdas ): Promise { - pdas = pdas ?? NTT.pdas(program.programId); - - const recipientAddress = - args.recipient ?? - (await getInboxItem(program, args.chain, args.nttMessage)) - .recipientAddress; + const [major, , ,] = parseVersion(program.idl.version); - const transferIx = await program.methods - .releaseInboundMint({ - revertOnDelay: args.revertOnDelay, - }) - .accountsStrict({ - common: { - payer: args.payer, - config: { config: pdas.configAccount() }, - inboxItem: pdas.inboxItemAccount(args.chain, args.nttMessage), - recipient: getAssociatedTokenAddressSync( - config.mint, - recipientAddress, - true, - config.tokenProgram - ), - mint: config.mint, - tokenAuthority: pdas.tokenAuthority(), - tokenProgram: config.tokenProgram, - custody: await custodyAccountAddress(pdas, config), - }, - }) - .instruction(); + pdas = pdas ?? NTT.pdas(program.programId); const mintInfo = await splToken.getMint( program.provider.connection, @@ -629,65 +562,24 @@ export namespace NTT { undefined, config.tokenProgram ); - const transferHook = splToken.getTransferHook(mintInfo); - - if (transferHook) { - const source = await custodyAccountAddress(pdas, config); - const mint = config.mint; - const destination = getAssociatedTokenAddressSync( - mint, - recipientAddress, - true, - config.tokenProgram - ); - const owner = pdas.tokenAuthority(); - await addExtraAccountMetasForExecute( - program.provider.connection, - transferIx, - transferHook.programId, - source, - mint, - destination, - owner, - // TODO(csongor): compute the amount that's passed into transfer. - // Leaving this 0 is fine unless the transfer hook accounts addresses - // depend on the amount (which is unlikely). - // If this turns out to be the case, the amount to put here is the - // untrimmed amount after removing dust. - 0 - ); + let multisigTokenAuthority: PublicKey | null = null; + if (!mintInfo.mintAuthority?.equals(pdas.tokenAuthority())) { + multisigTokenAuthority = mintInfo.mintAuthority; } - return transferIx; - } - - // TODO: document that if recipient is provided, then the instruction can be - // created before the inbox item is created (i.e. they can be put in the same tx) - export async function createReleaseInboundMintMultisigInstruction( - program: Program>, - config: NttBindings.Config, - args: { - payer: PublicKey; - chain: Chain; - nttMessage: Ntt.Message; - revertOnDelay: boolean; - multisig: PublicKey; - recipient?: PublicKey; - }, - pdas?: Pdas - ): Promise { - pdas = pdas ?? NTT.pdas(program.programId); - const recipientAddress = args.recipient ?? (await getInboxItem(program, args.chain, args.nttMessage)) .recipientAddress; const transferIx = await program.methods - .releaseInboundMintMultisig({ - revertOnDelay: args.revertOnDelay, + .releaseInboundMint({ + // NOTE: `revertOnDelay` is used for versions < 3.x.x + // For versions >= 3.x.x, `revertWhenNotReady` is used instead + revertOnDelay: args.revertWhenNotReady, + revertWhenNotReady: args.revertWhenNotReady, }) - .accountsStrict({ + .accounts({ common: { payer: args.payer, config: { config: pdas.configAccount() }, @@ -703,18 +595,14 @@ export namespace NTT { tokenProgram: config.tokenProgram, custody: await custodyAccountAddress(pdas, config), }, - multisig: args.multisig, + // NOTE: SPL Multisig token authority is only supported for versions >= 3.x.x + ...(major >= 3 && { + multisigTokenAuthority, + }), }) .instruction(); - const mintInfo = await splToken.getMint( - program.provider.connection, - config.mint, - undefined, - config.tokenProgram - ); const transferHook = splToken.getTransferHook(mintInfo); - if (transferHook) { const source = await custodyAccountAddress(pdas, config); const mint = config.mint; @@ -752,7 +640,7 @@ export namespace NTT { payer: PublicKey; chain: Chain; nttMessage: Ntt.Message; - revertOnDelay: boolean; + revertWhenNotReady: boolean; recipient?: PublicKey; }, pdas?: Pdas @@ -767,7 +655,10 @@ export namespace NTT { const transferIx = await program.methods .releaseInboundUnlock({ - revertOnDelay: args.revertOnDelay, + // NOTE: `revertOnDelay` is used for versions < 3.x.x + // For versions >= 3.x.x, `revertWhenNotReady` is used instead + revertOnDelay: args.revertWhenNotReady, + revertWhenNotReady: args.revertWhenNotReady, }) .accountsStrict({ common: { diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 62d78b532..b3b327963 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -630,7 +630,7 @@ export class SolanaNtt mint: PublicKey; mode: Ntt.Mode; outboundLimit: bigint; - multisig?: PublicKey; + multisigTokenAuthority?: PublicKey; } ) { const mintInfo = await this.connection.getAccountInfo(args.mint); @@ -641,30 +641,18 @@ export class SolanaNtt const payer = new SolanaAddress(sender).unwrap(); - const ix = args.multisig - ? await NTT.createInitializeMultisigInstruction( - this.program, - { - ...args, - payer, - owner: payer, - chain: this.chain, - tokenProgram: mintInfo.owner, - multisig: args.multisig, - }, - this.pdas - ) - : await NTT.createInitializeInstruction( - this.program, - { - ...args, - payer, - owner: payer, - chain: this.chain, - tokenProgram: mintInfo.owner, - }, - this.pdas - ); + const ix = await NTT.createInitializeInstruction( + this.program, + { + ...args, + payer, + owner: payer, + chain: this.chain, + tokenProgram: mintInfo.owner, + multisigTokenAuthority: args.multisigTokenAuthority, + }, + this.pdas + ); const tx = new Transaction(); tx.feePayer = payer; @@ -966,11 +954,7 @@ export class SolanaNtt } } - async *redeem( - attestations: Ntt.Attestation[], - payer: AccountAddress, - multisig?: PublicKey - ) { + async *redeem(attestations: Ntt.Attestation[], payer: AccountAddress) { const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); @@ -1025,27 +1009,17 @@ export class SolanaNtt nttMessage.payload.recipientAddress.toUint8Array() ), chain: emitterChain, - revertOnDelay: false, + // NOTE: this acts as `revertOnDelay` for versions < 3.x.x + revertWhenNotReady: false, }; let releaseIx = config.mode.locking != null ? NTT.createReleaseInboundUnlockInstruction(this.program, config, { ...releaseArgs, }) - : multisig - ? NTT.createReleaseInboundMintMultisigInstruction( - this.program, - config, - { - ...releaseArgs, - multisig, - } - ) - : NTT.createReleaseInboundMintInstruction( - this.program, - config, - releaseArgs - ); + : NTT.createReleaseInboundMintInstruction(this.program, config, { + ...releaseArgs, + }); const tx = new Transaction(); tx.feePayer = senderAddress; @@ -1208,7 +1182,8 @@ export class SolanaNtt transceiverMessage.payload.recipientAddress.toUint8Array() ), chain: fromChain, - revertOnDelay: false, + // NOTE: this acts as `revertOnDelay` for versions < 3.x.x + revertWhenNotReady: false, }; tx.add(