From bd2de470a9a965b3f99b1441a37af073c37ef55e Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 00:42:01 -0500 Subject: [PATCH 01/14] Replace `initialize_multisig` ix with optional multisig account in `Initialize` --- .../src/instructions/initialize.rs | 61 ++++++------------- .../example-native-token-transfers/src/lib.rs | 7 --- 2 files changed, 19 insertions(+), 49 deletions(-) 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/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index c581a35dd..def0091e4 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) } From 97a028d8797604475c31a7c3e16353925de5b27b Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 00:42:54 -0500 Subject: [PATCH 02/14] Replace `release_inbound_mint_multisig` ix with optional multisig account in `ReleaseInboundMint` --- .../src/instructions/release_inbound.rs | 142 ++++++++---------- .../example-native-token-transfers/src/lib.rs | 7 - 2 files changed, 62 insertions(+), 87 deletions(-) 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..cae1b1f2f 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 @@ -65,6 +65,13 @@ 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. @@ -106,18 +113,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 +148,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(()) } @@ -258,6 +239,7 @@ pub fn release_inbound_unlock<'info>( )?; Ok(()) } + fn release_inbox_item( inbox_item: &mut InboxItem, revert_on_delay: bool, diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index def0091e4..49d6d802f 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -109,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, From a8aac6a850fb7ef1aa0a669ca0e19b0de5fad3e1 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 00:43:30 -0500 Subject: [PATCH 03/14] Update cargo test to match new `Initialize` struct --- .../example-native-token-transfers/tests/common/setup.rs | 1 + .../tests/sdk/instructions/initialize.rs | 2 ++ 2 files changed, 3 insertions(+) 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(), From a22e43c7ccbf171740b5907c2a1794ebb39d8b33 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 00:44:20 -0500 Subject: [PATCH 04/14] Remove redundant `.to_account_info()` call --- .../src/instructions/admin/transfer_token_authority.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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]]], ), From 633b9030441f601284d50e4116c8280e34ba4b0c Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 00:44:54 -0500 Subject: [PATCH 05/14] Update IDL --- .../json/example_native_token_transfers.json | 175 +-------- .../ts/example_native_token_transfers.ts | 350 +----------------- 2 files changed, 27 insertions(+), 498 deletions(-) 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..ef3120968 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": [ 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..685be9aec 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": [ @@ -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": [ From 2ee1eb95e8d7308109bf7b13ffa31d9090c0dabd Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 00:45:34 -0500 Subject: [PATCH 06/14] Update TS test case and SDK --- solana/tests/anchor.test.ts | 2 +- solana/ts/lib/ntt.ts | 148 ++++-------------------------------- solana/ts/sdk/ntt.ts | 58 +++++--------- 3 files changed, 34 insertions(+), 174 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index 00004c973..e22d334d2 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); diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 67ec69bff..c81f092fc 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 support 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( @@ -590,92 +549,12 @@ export namespace NTT { nttMessage: Ntt.Message; revertOnDelay: boolean; recipient?: PublicKey; + multisigTokenAuthority?: 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 - .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(); - - 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; - 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 - ); - } - - return transferIx; - } + const [major, , ,] = parseVersion(program.idl.version); - // 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 = @@ -684,10 +563,10 @@ export namespace NTT { .recipientAddress; const transferIx = await program.methods - .releaseInboundMintMultisig({ + .releaseInboundMint({ revertOnDelay: args.revertOnDelay, }) - .accountsStrict({ + .accounts({ common: { payer: args.payer, config: { config: pdas.configAccount() }, @@ -703,7 +582,10 @@ export namespace NTT { tokenProgram: config.tokenProgram, custody: await custodyAccountAddress(pdas, config), }, - multisig: args.multisig, + // NOTE: SPL Multisig token authority is only support for versions >= 3.x.x + ...(major >= 3 && { + multisigTokenAuthority: args.multisigTokenAuthority ?? null, + }), }) .instruction(); diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 62d78b532..fb290c469 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; @@ -969,7 +957,7 @@ export class SolanaNtt async *redeem( attestations: Ntt.Attestation[], payer: AccountAddress, - multisig?: PublicKey + multisigTokenAuthority?: PublicKey ) { const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); @@ -1032,20 +1020,10 @@ export class SolanaNtt ? 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, + multisigTokenAuthority, + }); const tx = new Transaction(); tx.feePayer = senderAddress; From 1f3b1ff463e3acb86ba108b31ee1eb745dfeee4e Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 11:11:45 -0500 Subject: [PATCH 07/14] Derive multisigTokenAuthority from token mint info --- solana/tests/anchor.test.ts | 2 +- solana/ts/lib/ntt.ts | 21 ++++++++++++--------- solana/ts/sdk/ntt.ts | 7 +------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index e22d334d2..9f9cb49e1 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -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/lib/ntt.ts b/solana/ts/lib/ntt.ts index c81f092fc..7f6441272 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -549,7 +549,6 @@ export namespace NTT { nttMessage: Ntt.Message; revertOnDelay: boolean; recipient?: PublicKey; - multisigTokenAuthority?: PublicKey; }, pdas?: Pdas ): Promise { @@ -557,6 +556,17 @@ export namespace NTT { pdas = pdas ?? NTT.pdas(program.programId); + const mintInfo = await splToken.getMint( + program.provider.connection, + config.mint, + undefined, + config.tokenProgram + ); + let multisigTokenAuthority: PublicKey | null = null; + if (!mintInfo.mintAuthority?.equals(pdas.tokenAuthority())) { + multisigTokenAuthority = mintInfo.mintAuthority; + } + const recipientAddress = args.recipient ?? (await getInboxItem(program, args.chain, args.nttMessage)) @@ -584,19 +594,12 @@ export namespace NTT { }, // NOTE: SPL Multisig token authority is only support for versions >= 3.x.x ...(major >= 3 && { - multisigTokenAuthority: args.multisigTokenAuthority ?? null, + 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; diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index fb290c469..e19d666a4 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -954,11 +954,7 @@ export class SolanaNtt } } - async *redeem( - attestations: Ntt.Attestation[], - payer: AccountAddress, - multisigTokenAuthority?: PublicKey - ) { + async *redeem(attestations: Ntt.Attestation[], payer: AccountAddress) { const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); @@ -1022,7 +1018,6 @@ export class SolanaNtt }) : NTT.createReleaseInboundMintInstruction(this.program, config, { ...releaseArgs, - multisigTokenAuthority, }); const tx = new Transaction(); From 68d7003ded315b2a4fa01e4cbd366e451b3a627b Mon Sep 17 00:00:00 2001 From: John Saigle Date: Fri, 22 Mar 2024 15:06:51 -0400 Subject: [PATCH 08/14] 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. --- .../src/queue/inbox.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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..2218dc956 100644 --- a/solana/programs/example-native-token-transfers/src/queue/inbox.rs +++ b/solana/programs/example-native-token-transfers/src/queue/inbox.rs @@ -35,12 +35,23 @@ 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. + /// + /// # Errors + /// + /// Returns errors when the item cannot be released, i.e. when its status is not + /// `ReleaseAfter`: + /// - returns [`NTTError::TransferNotApproved`] if [`ReleaseStatus::NotApproved`] + /// - returns [`NTTError::TransferAlreadyRedeemed`] if [`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(); match self.release_status { - ReleaseStatus::NotApproved => Ok(false), + ReleaseStatus::NotApproved => Err(NTTError::TransferNotApproved.into()), ReleaseStatus::ReleaseAfter(release_timestamp) => { if release_timestamp > now { return Ok(false); From e6852fd9507df9a4481938b6177020ccbf74ed59 Mon Sep 17 00:00:00 2001 From: John Saigle Date: Fri, 22 Mar 2024 17:48:11 -0400 Subject: [PATCH 09/14] solana: change comment to revert_on_delay --- .../src/instructions/release_inbound.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 cae1b1f2f..049063e19 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 @@ -75,8 +75,8 @@ pub struct ReleaseInboundMint<'info> { } /// 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_on_delay` is true, the transaction will revert if the +/// release timestamp has not been reached. When `revert_on_delay` 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 @@ -206,8 +206,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_on_delay` is true, the transaction will revert if the +/// release timestamp has not been reached. When `revert_on_delay` 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 From 2c29ce03a8447b5cf45cc7417b1fb9f5dd170ff0 Mon Sep 17 00:00:00 2001 From: John Saigle Date: Thu, 28 Mar 2024 11:12:40 -0400 Subject: [PATCH 10/14] solana: Let redeem code return error instead of try_release --- .../src/instructions/release_inbound.rs | 32 +++++++++++++------ .../src/queue/inbox.rs | 2 +- 2 files changed, 23 insertions(+), 11 deletions(-) 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 049063e19..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 @@ -75,8 +75,8 @@ pub struct ReleaseInboundMint<'info> { } /// Release an inbound transfer and mint the tokens to the recipient. -/// When `revert_on_delay` is true, the transaction will revert if the -/// release timestamp has not been reached. When `revert_on_delay` 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 @@ -85,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(()); } @@ -206,8 +209,8 @@ pub struct ReleaseInboundUnlock<'info> { } /// Release an inbound transfer and unlock the tokens to the recipient. -/// When `revert_on_delay` is true, the transaction will revert if the -/// release timestamp has not been reached. When `revert_on_delay` 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 @@ -216,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(()); } @@ -242,12 +248,18 @@ pub fn release_inbound_unlock<'info>( 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/queue/inbox.rs b/solana/programs/example-native-token-transfers/src/queue/inbox.rs index 2218dc956..acdaa2e77 100644 --- a/solana/programs/example-native-token-transfers/src/queue/inbox.rs +++ b/solana/programs/example-native-token-transfers/src/queue/inbox.rs @@ -51,7 +51,7 @@ impl InboxItem { let now = current_timestamp(); match self.release_status { - ReleaseStatus::NotApproved => Err(NTTError::TransferNotApproved.into()), + ReleaseStatus::NotApproved => Ok(false), ReleaseStatus::ReleaseAfter(release_timestamp) => { if release_timestamp > now { return Ok(false); From 59f4413b8d12dd786d7e3d817d034b8ae9e67fdd Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 14:11:36 -0500 Subject: [PATCH 11/14] nit: Fix grammar --- solana/ts/lib/ntt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 7f6441272..ca64fbe64 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -267,7 +267,7 @@ export namespace NTT { rateLimit: pdas.outboxRateLimitAccount(), tokenProgram: args.tokenProgram, tokenAuthority: pdas.tokenAuthority(), - // NOTE: SPL Multisig token authority is only support for versions >= 3.x.x + // NOTE: SPL Multisig token authority is only supported for versions >= 3.x.x ...(major >= 3 && { multisigTokenAuthority: args.multisigTokenAuthority ?? null, }), @@ -592,7 +592,7 @@ export namespace NTT { tokenProgram: config.tokenProgram, custody: await custodyAccountAddress(pdas, config), }, - // NOTE: SPL Multisig token authority is only support for versions >= 3.x.x + // NOTE: SPL Multisig token authority is only supported for versions >= 3.x.x ...(major >= 3 && { multisigTokenAuthority, }), From 786498aef66b4d5fbfd4ef9ac97e585d960850dd Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 14:14:16 -0500 Subject: [PATCH 12/14] Update IDL --- solana/ts/idl/3_0_0/json/example_native_token_transfers.json | 2 +- solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 ef3120968..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 @@ -2129,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 685be9aec..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 @@ -2129,7 +2129,7 @@ export type ExampleNativeTokenTransfers = { "kind": "struct", "fields": [ { - "name": "revertOnDelay", + "name": "revertWhenNotReady", "type": "bool" } ] @@ -4624,7 +4624,7 @@ export const IDL: ExampleNativeTokenTransfers = { "kind": "struct", "fields": [ { - "name": "revertOnDelay", + "name": "revertWhenNotReady", "type": "bool" } ] From 5bb38bea9db05f21d7ce1409853b32b7d5df61bd Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 14:14:50 -0500 Subject: [PATCH 13/14] Update TS SDK to use renamed args --- solana/ts/lib/ntt.ts | 14 ++++++++++---- solana/ts/sdk/ntt.ts | 6 ++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index ca64fbe64..eff78a805 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -547,7 +547,7 @@ export namespace NTT { payer: PublicKey; chain: Chain; nttMessage: Ntt.Message; - revertOnDelay: boolean; + revertWhenNotReady: boolean; recipient?: PublicKey; }, pdas?: Pdas @@ -574,7 +574,10 @@ export namespace NTT { const transferIx = await program.methods .releaseInboundMint({ - 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, }) .accounts({ common: { @@ -637,7 +640,7 @@ export namespace NTT { payer: PublicKey; chain: Chain; nttMessage: Ntt.Message; - revertOnDelay: boolean; + revertWhenNotReady: boolean; recipient?: PublicKey; }, pdas?: Pdas @@ -652,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 e19d666a4..b3b327963 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -1009,7 +1009,8 @@ 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 @@ -1181,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( From cc573c3d082333ddd23c3a52f37d5e323a3dca6c Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 5 Mar 2025 17:04:32 -0500 Subject: [PATCH 14/14] Update comment to match function logic --- .../src/queue/inbox.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 acdaa2e77..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,17 +35,15 @@ impl InboxItem { pub const SEED_PREFIX: &'static [u8] = b"inbox_item"; /// Attempt to release the transfer. - /// 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::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 errors when the item cannot be released, i.e. when its status is not - /// `ReleaseAfter`: - /// - returns [`NTTError::TransferNotApproved`] if [`ReleaseStatus::NotApproved`] - /// - returns [`NTTError::TransferAlreadyRedeemed`] if [`ReleaseStatus::Released`]. This is - /// important to prevent a single transfer from being redeemed multiple times, which would + /// 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();