From ee039686a94cd4b06d19ed65bd1b9f9bee3ea171 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Tue, 5 Nov 2024 23:41:17 -0500 Subject: [PATCH 01/19] solana: Add multisig versions of `initialize` and `release_inbound_mint` --- .../src/instructions/initialize_multisig.rs | 127 +++++++++++++++ .../src/instructions/mod.rs | 4 + .../instructions/release_inbound_multisig.rs | 152 ++++++++++++++++++ .../example-native-token-transfers/src/lib.rs | 14 ++ 4 files changed, 297 insertions(+) create mode 100644 solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs create mode 100644 solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs diff --git a/solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs b/solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs new file mode 100644 index 000000000..3616fad00 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs @@ -0,0 +1,127 @@ +use anchor_lang::prelude::*; +use anchor_spl::{associated_token::AssociatedToken, token_interface}; +use ntt_messages::{chain_id::ChainId, mode::Mode}; +use wormhole_solana_utils::cpi::bpf_loader_upgradeable::BpfLoaderUpgradeable; + +#[cfg(feature = "idl-build")] +use crate::messages::Hack; + +use crate::{ + bitmap::Bitmap, + error::NTTError, + queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState}, +}; + +#[derive(Accounts)] +#[instruction(args: InitializeMultisigArgs)] +pub struct InitializeMultisig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account(address = program_data.upgrade_authority_address.unwrap_or_default())] + pub deployer: Signer<'info>, + + #[account( + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable_program, + )] + program_data: Account<'info, ProgramData>, + + #[account( + init, + space = 8 + crate::config::Config::INIT_SPACE, + payer = payer, + seeds = [crate::config::Config::SEED_PREFIX], + bump + )] + pub config: Box>, + + #[account( + constraint = + args.mode == Mode::Locking + || mint.mint_authority.unwrap() == multisig.key() + @ NTTError::InvalidMintAuthority, + )] + pub mint: Box>, + + #[account( + init, + payer = payer, + space = 8 + OutboxRateLimit::INIT_SPACE, + seeds = [OutboxRateLimit::SEED_PREFIX], + bump, + )] + pub rate_limit: Account<'info, OutboxRateLimit>, + + #[account()] + /// CHECK: multisig is mint authority + pub multisig: UncheckedAccount<'info>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK: [`token_authority`] is checked against the custody account and the [`mint`]'s mint_authority + /// 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. + pub token_authority: AccountInfo<'info>, + + #[account( + init_if_needed, + payer = payer, + associated_token::mint = mint, + associated_token::authority = token_authority, + associated_token::token_program = token_program, + )] + /// The custody account that holds tokens in locking mode and temporarily + /// holds tokens in burning mode. + /// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`] + /// function if the token account has already been created. + pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, + + /// CHECK: checked to be the appropriate token program when initialising the + /// associated token account for the given mint. + pub token_program: Interface<'info, token_interface::TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, + + system_program: Program<'info, System>, +} + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct InitializeMultisigArgs { + pub chain_id: u16, + pub limit: u64, + pub mode: ntt_messages::mode::Mode, +} + +pub fn initialize_multisig( + ctx: Context, + args: InitializeMultisigArgs, +) -> Result<()> { + ctx.accounts.config.set_inner(crate::config::Config { + bump: ctx.bumps.config, + mint: ctx.accounts.mint.key(), + token_program: ctx.accounts.token_program.key(), + mode: args.mode, + chain_id: ChainId { id: args.chain_id }, + owner: ctx.accounts.deployer.key(), + pending_owner: None, + paused: false, + next_transceiver_id: 0, + // NOTE: can't be changed for now + threshold: 1, + enabled_transceivers: Bitmap::new(), + custody: ctx.accounts.custody.key(), + }); + + ctx.accounts.rate_limit.set_inner(OutboxRateLimit { + rate_limit: RateLimitState::new(args.limit), + }); + + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/instructions/mod.rs b/solana/programs/example-native-token-transfers/src/instructions/mod.rs index d9fec7be5..5209f70a5 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/mod.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/mod.rs @@ -1,15 +1,19 @@ pub mod admin; pub mod initialize; +pub mod initialize_multisig; pub mod luts; pub mod mark_outbox_item_as_released; pub mod redeem; pub mod release_inbound; +pub mod release_inbound_multisig; pub mod transfer; pub use admin::*; pub use initialize::*; +pub use initialize_multisig::*; pub use luts::*; pub use mark_outbox_item_as_released::*; pub use redeem::*; pub use release_inbound::*; +pub use release_inbound_multisig::*; pub use transfer::*; diff --git a/solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs b/solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs new file mode 100644 index 000000000..fda617706 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs @@ -0,0 +1,152 @@ +use anchor_lang::prelude::*; +use anchor_spl::token_interface; +use ntt_messages::mode::Mode; +use spl_token_2022::onchain; + +use crate::{ + config::*, + error::NTTError, + queue::inbox::{InboxItem, ReleaseStatus}, +}; + +#[derive(Accounts)] +pub struct ReleaseInboundMultisig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + pub config: NotPausedConfig<'info>, + + #[account(mut)] + pub inbox_item: Account<'info, InboxItem>, + + #[account( + mut, + associated_token::authority = inbox_item.recipient_address, + associated_token::mint = mint, + associated_token::token_program = token_program, + )] + pub recipient: InterfaceAccount<'info, token_interface::TokenAccount>, + + /// CHECK: multisig account should be mint authority + #[account(constraint = mint.mint_authority.unwrap() == multisig.key())] + pub multisig: UncheckedAccount<'info>, + + #[account( + seeds = [crate::TOKEN_AUTHORITY_SEED], + bump, + )] + /// CHECK The seeds constraint ensures that this is the correct address + pub token_authority: UncheckedAccount<'info>, + + #[account( + mut, + address = config.mint, + )] + /// CHECK: the mint address matches the config + pub mint: InterfaceAccount<'info, token_interface::Mint>, + + pub token_program: Interface<'info, token_interface::TokenInterface>, + + /// CHECK: the token program checks if this indeed the right authority for the mint + #[account( + mut, + address = config.custody + )] + pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, +} + +#[derive(AnchorDeserialize, AnchorSerialize)] +pub struct ReleaseInboundMultisigArgs { + pub revert_on_delay: bool, +} + +// Burn/mint + +#[derive(Accounts)] +pub struct ReleaseInboundMultisigMint<'info> { + #[account( + constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, + )] + common: ReleaseInboundMultisig<'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 +/// 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 +/// is attempted optimistically. +pub fn release_inbound_multisig_mint<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>, + args: ReleaseInboundMultisigArgs, +) -> Result<()> { + let inbox_item = &mut ctx.accounts.common.inbox_item; + + let released = inbox_item.try_release()?; + + if !released { + if args.revert_on_delay { + return Err(NTTError::CantReleaseYet.into()); + } else { + return Ok(()); + } + } + + assert!(inbox_item.release_status == ReleaseStatus::Released); + + // 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). + + // Step 1: mint tokens to the custody account + let ix = spl_token_2022::instruction::mint_to( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + &ctx.accounts.common.custody.key(), + &ctx.accounts.common.multisig.key(), + &[&ctx.accounts.common.token_authority.key()], + inbox_item.amount, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.common.custody.to_account_info(), + ctx.accounts.common.mint.to_account_info(), + ctx.accounts.common.token_authority.to_account_info(), + ctx.accounts.common.multisig.to_account_info(), + ], + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + )?; + + // 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, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + )?; + Ok(()) +} diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 5ad846e7a..8c60132df 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -75,6 +75,13 @@ pub mod example_native_token_transfers { instructions::initialize(ctx, args) } + pub fn initialize_multisig( + ctx: Context, + args: InitializeMultisigArgs, + ) -> Result<()> { + instructions::initialize_multisig(ctx, args) + } + pub fn initialize_lut(ctx: Context, recent_slot: u64) -> Result<()> { instructions::initialize_lut(ctx, recent_slot) } @@ -115,6 +122,13 @@ pub mod example_native_token_transfers { instructions::release_inbound_unlock(ctx, args) } + pub fn release_inbound_multisig_mint<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>, + args: ReleaseInboundMultisigArgs, + ) -> Result<()> { + instructions::release_inbound_multisig_mint(ctx, args) + } + pub fn transfer_ownership(ctx: Context) -> Result<()> { instructions::transfer_ownership(ctx) } From f12ea29790b79df20264295eac026b0f98f34ba2 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Tue, 5 Nov 2024 23:48:45 -0500 Subject: [PATCH 02/19] solana: Update TS test and IDL --- solana/tests/anchor.test.ts | 85 +++- .../json/example_native_token_transfers.json | 198 +++++++++ .../ts/example_native_token_transfers.ts | 396 ++++++++++++++++++ solana/ts/lib/ntt.ts | 129 ++++++ solana/ts/sdk/ntt.ts | 55 ++- 5 files changed, 826 insertions(+), 37 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index 16ba7319b..6f4e005e1 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -24,12 +24,6 @@ import { import { SolanaWormholeCore } from "@wormhole-foundation/sdk-solana-core"; import * as fs from "fs"; -import { - PublicKey, - SystemProgram, - Transaction, - sendAndConfirmTransaction, -} from "@solana/web3.js"; import { DummyTransferHook } from "../ts/idl/1_0_0/ts/dummy_transfer_hook.js"; import { getTransceiverProgram, IdlVersion, NTT } from "../ts/index.js"; import { derivePda } from "../ts/lib/utils.js"; @@ -41,9 +35,9 @@ const VERSION: IdlVersion = "3.0.0"; const GUARDIAN_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; const CORE_BRIDGE_ADDRESS = contracts.coreBridge("Mainnet", "Solana"); -const NTT_ADDRESS: PublicKey = +const NTT_ADDRESS: anchor.web3.PublicKey = anchor.workspace.ExampleNativeTokenTransfers.programId; -const WH_TRANSCEIVER_ADDRESS: PublicKey = +const WH_TRANSCEIVER_ADDRESS: anchor.web3.PublicKey = anchor.workspace.NttTransceiver.programId; async function signSendWait( @@ -102,12 +96,12 @@ const mint = anchor.web3.Keypair.generate(); const dummyTransferHook = anchor.workspace .DummyTransferHook as anchor.Program; -const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( +const [extraAccountMetaListPDA] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], dummyTransferHook.programId ); -const [counterPDA] = PublicKey.findProgramAddressSync( +const [counterPDA] = anchor.web3.PublicKey.findProgramAddressSync( [Buffer.from("counter")], dummyTransferHook.programId ); @@ -135,6 +129,7 @@ describe("example-native-token-transfers", () => { let ntt: SolanaNtt<"Devnet", "Solana">; let signer: Signer; let sender: AccountAddress<"Solana">; + let multisig: anchor.web3.PublicKey; let tokenAddress: string; beforeAll(async () => { @@ -150,8 +145,8 @@ describe("example-native-token-transfers", () => { mintLen ); - const transaction = new Transaction().add( - SystemProgram.createAccount({ + const transaction = new anchor.web3.Transaction().add( + anchor.web3.SystemProgram.createAccount({ fromPubkey: payer.publicKey, newAccountPubkey: mint.publicKey, space: mintLen, @@ -178,9 +173,14 @@ describe("example-native-token-transfers", () => { transaction.feePayer = payer.publicKey; transaction.recentBlockhash = blockhash; - await sendAndConfirmTransaction(connection, transaction, [payer, mint], { - commitment: "confirmed", - }); + await anchor.web3.sendAndConfirmTransaction( + connection, + transaction, + [payer, mint], + { + commitment: "confirmed", + } + ); tokenAccount = await spl.createAssociatedTokenAccount( connection, @@ -231,13 +231,22 @@ describe("example-native-token-transfers", () => { describe("Burning", () => { beforeAll(async () => { try { + multisig = await spl.createMultisig( + connection, + payer, + [owner.publicKey, ntt.pdas.tokenAuthority()], + 1, + anchor.web3.Keypair.generate(), + undefined, + TOKEN_PROGRAM + ); await spl.setAuthority( connection, payer, mint.publicKey, owner, spl.AuthorityType.MintTokens, - ntt.pdas.tokenAuthority(), + multisig, [], undefined, TOKEN_PROGRAM @@ -248,6 +257,7 @@ describe("example-native-token-transfers", () => { mint: mint.publicKey, outboundLimit: 1000000n, mode: "burning", + multisig, }); await signSendWait(ctx, initTxs, signer); @@ -285,11 +295,11 @@ describe("example-native-token-transfers", () => { extraAccountMetaList: extraAccountMetaListPDA, tokenProgram: TOKEN_PROGRAM, associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, + systemProgram: anchor.web3.SystemProgram.programId, }) .instruction(); - const transaction = new Transaction().add( + const transaction = new anchor.web3.Transaction().add( initializeExtraAccountMetaListInstruction ); transaction.feePayer = payer.publicKey; @@ -297,9 +307,14 @@ describe("example-native-token-transfers", () => { transaction.recentBlockhash = blockhash; transaction.sign(payer); - await sendAndConfirmTransaction(connection, transaction, [payer], { - commitment: "confirmed", - }); + await anchor.web3.sendAndConfirmTransaction( + connection, + transaction, + [payer], + { + commitment: "confirmed", + } + ); }); test("Can send tokens", async () => { @@ -391,7 +406,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); + const redeemTxs = ntt.redeem([vaa], sender, multisig); try { await signSendWait(ctx, redeemTxs, signer); } catch (e) { @@ -401,6 +416,32 @@ describe("example-native-token-transfers", () => { expect((await counterValue()).toString()).toEqual("2"); }); + + it("Can mint independently", async () => { + const dest = await spl.getOrCreateAssociatedTokenAccount( + connection, + payer, + mint.publicKey, + anchor.web3.Keypair.generate().publicKey, + false, + undefined, + undefined, + TOKEN_PROGRAM + ); + await spl.mintTo( + connection, + payer, + mint.publicKey, + dest.address, + multisig, + 1, + [owner], + undefined, + TOKEN_PROGRAM + ); + const balance = await connection.getTokenAccountBalance(dest.address); + expect(balance.value.amount.toString()).toBe("1"); + }); }); describe("Static Checks", () => { 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 fd11bb705..4601c5d57 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 @@ -90,6 +90,99 @@ } ] }, + { + "name": "initializeMultisig", + "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": "multisig", + "isMut": false, + "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 + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "InitializeMultisigArgs" + } + } + ] + }, { "name": "initializeLut", "accounts": [ @@ -616,6 +709,77 @@ } ] }, + { + "name": "releaseInboundMultisigMint", + "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": "multisig", + "isMut": false, + "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": "ReleaseInboundMultisigArgs" + } + } + ] + }, { "name": "transferOwnership", "accounts": [ @@ -1920,6 +2084,28 @@ ] } }, + { + "name": "InitializeMultisigArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "mode", + "type": { + "defined": "Mode" + } + } + ] + } + }, { "name": "RedeemArgs", "type": { @@ -1939,6 +2125,18 @@ ] } }, + { + "name": "ReleaseInboundMultisigArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, { "name": "TransferArgs", "type": { 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 c098e6e5f..268aa6589 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 @@ -90,6 +90,99 @@ export type ExampleNativeTokenTransfers = { } ] }, + { + "name": "initializeMultisig", + "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": "multisig", + "isMut": false, + "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 + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "InitializeMultisigArgs" + } + } + ] + }, { "name": "initializeLut", "accounts": [ @@ -616,6 +709,77 @@ export type ExampleNativeTokenTransfers = { } ] }, + { + "name": "releaseInboundMultisigMint", + "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": "multisig", + "isMut": false, + "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": "ReleaseInboundMultisigArgs" + } + } + ] + }, { "name": "transferOwnership", "accounts": [ @@ -1920,6 +2084,28 @@ export type ExampleNativeTokenTransfers = { ] } }, + { + "name": "InitializeMultisigArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "mode", + "type": { + "defined": "Mode" + } + } + ] + } + }, { "name": "RedeemArgs", "type": { @@ -1939,6 +2125,18 @@ export type ExampleNativeTokenTransfers = { ] } }, + { + "name": "ReleaseInboundMultisigArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, { "name": "TransferArgs", "type": { @@ -2374,6 +2572,99 @@ export const IDL: ExampleNativeTokenTransfers = { } ] }, + { + "name": "initializeMultisig", + "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": "multisig", + "isMut": false, + "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 + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "InitializeMultisigArgs" + } + } + ] + }, { "name": "initializeLut", "accounts": [ @@ -2900,6 +3191,77 @@ export const IDL: ExampleNativeTokenTransfers = { } ] }, + { + "name": "releaseInboundMultisigMint", + "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": "multisig", + "isMut": false, + "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": "ReleaseInboundMultisigArgs" + } + } + ] + }, { "name": "transferOwnership", "accounts": [ @@ -4204,6 +4566,28 @@ export const IDL: ExampleNativeTokenTransfers = { ] } }, + { + "name": "InitializeMultisigArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "chainId", + "type": "u16" + }, + { + "name": "limit", + "type": "u64" + }, + { + "name": "mode", + "type": { + "defined": "Mode" + } + } + ] + } + }, { "name": "RedeemArgs", "type": { @@ -4223,6 +4607,18 @@ export const IDL: ExampleNativeTokenTransfers = { ] } }, + { + "name": "ReleaseInboundMultisigArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "revertOnDelay", + "type": "bool" + } + ] + } + }, { "name": "TransferArgs", "type": { diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 147b9a0d3..1bbb8204d 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -277,6 +277,51 @@ 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({ + payer: args.payer, + deployer: args.owner, + programData: programDataAddress(program.programId), + config: pdas.configAccount(), + mint: args.mint, + rateLimit: pdas.outboxRateLimitAccount(), + multisig: args.multisig, + tokenProgram: args.tokenProgram, + tokenAuthority: pdas.tokenAuthority(), + custody: await NTT.custodyAccountAddress( + pdas, + args.mint, + args.tokenProgram + ), + bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_PROGRAM_ID, + associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, + }) + .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( @@ -614,6 +659,90 @@ export namespace NTT { 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 createReleaseInboundMultisigMintInstruction( + 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 + .releaseInboundMultisigMint({ + 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), + multisig: args.multisig, + }, + }) + .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; + } + export async function createReleaseInboundUnlockInstruction( program: Program>, config: NttBindings.Config, diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index 3e8982d2b..e106cc774 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -630,6 +630,7 @@ export class SolanaNtt mint: PublicKey; mode: Ntt.Mode; outboundLimit: bigint; + multisig?: PublicKey; } ) { const mintInfo = await this.connection.getAccountInfo(args.mint); @@ -640,17 +641,30 @@ export class SolanaNtt const payer = new SolanaAddress(sender).unwrap(); - const ix = await NTT.createInitializeInstruction( - this.program, - { - ...args, - payer, - owner: payer, - chain: this.chain, - tokenProgram: mintInfo.owner, - }, - this.pdas - ); + 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 tx = new Transaction(); tx.feePayer = payer; @@ -930,7 +944,11 @@ export class SolanaNtt } } - async *redeem(attestations: Ntt.Attestation[], payer: AccountAddress) { + async *redeem( + attestations: Ntt.Attestation[], + payer: AccountAddress, + multisig?: PublicKey + ) { const config = await this.getConfig(); if (config.paused) throw new Error("Contract is paused"); @@ -987,12 +1005,19 @@ export class SolanaNtt chain: emitterChain, revertOnDelay: false, }; - const releaseIx = + let releaseIx = config.mode.locking != null - ? NTT.createReleaseInboundUnlockInstruction( + ? NTT.createReleaseInboundUnlockInstruction(this.program, config, { + ...releaseArgs, + }) + : multisig + ? NTT.createReleaseInboundMultisigMintInstruction( this.program, config, - releaseArgs + { + ...releaseArgs, + multisig, + } ) : NTT.createReleaseInboundMintInstruction( this.program, From 83a7f1360a845017dc04b708387beef9037608e0 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 6 Dec 2024 19:59:34 -0500 Subject: [PATCH 03/19] solana[WIP]: Refactor to reduce code duplication --- .../src/instructions/initialize.rs | 83 ++++++++--- .../src/instructions/mod.rs | 4 - .../src/instructions/release_inbound.rs | 137 +++++++++++++++--- .../example-native-token-transfers/src/lib.rs | 20 +-- 4 files changed, 196 insertions(+), 48 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 bcc37de0d..bb628306a 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -37,12 +37,7 @@ pub struct Initialize<'info> { )] pub config: Box>, - #[account( - constraint = - args.mode == Mode::Locking - || mint.mint_authority.unwrap() == token_authority.key() - @ NTTError::InvalidMintAuthority, - )] + #[account()] pub mint: Box>, #[account( @@ -95,25 +90,79 @@ pub struct InitializeArgs { pub mode: ntt_messages::mode::Mode, } -pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { - ctx.accounts.config.set_inner(crate::config::Config { - bump: ctx.bumps.config, - mint: ctx.accounts.mint.key(), - token_program: ctx.accounts.token_program.key(), - mode: args.mode, - chain_id: ChainId { id: args.chain_id }, - owner: ctx.accounts.deployer.key(), +#[derive(Accounts)] +#[instruction(args: InitializeArgs)] +pub struct InitializeDefault<'info> { + #[account( + constraint = + args.mode == Mode::Locking + || common.mint.mint_authority.unwrap() == common.token_authority.key() + @ NTTError::InvalidMintAuthority, + )] + pub common: Initialize<'info>, +} + +pub fn initialize(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, + ) +} + +#[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()] + /// CHECK: multisig is mint authority + pub multisig: UncheckedAccount<'info>, +} + +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, + chain_id: u16, + limit: u64, + mode: ntt_messages::mode::Mode, +) -> Result<()> { + common.config.set_inner(crate::config::Config { + bump: config_bump, + mint: common.mint.key(), + token_program: common.token_program.key(), + mode, + chain_id: ChainId { id: chain_id }, + owner: common.deployer.key(), pending_owner: None, paused: false, next_transceiver_id: 0, // NOTE: can't be changed for now threshold: 1, enabled_transceivers: Bitmap::new(), - custody: ctx.accounts.custody.key(), + custody: common.custody.key(), }); - ctx.accounts.rate_limit.set_inner(OutboxRateLimit { - rate_limit: RateLimitState::new(args.limit), + common.rate_limit.set_inner(OutboxRateLimit { + rate_limit: RateLimitState::new(limit), }); Ok(()) diff --git a/solana/programs/example-native-token-transfers/src/instructions/mod.rs b/solana/programs/example-native-token-transfers/src/instructions/mod.rs index 5209f70a5..d9fec7be5 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/mod.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/mod.rs @@ -1,19 +1,15 @@ pub mod admin; pub mod initialize; -pub mod initialize_multisig; pub mod luts; pub mod mark_outbox_item_as_released; pub mod redeem; pub mod release_inbound; -pub mod release_inbound_multisig; pub mod transfer; pub use admin::*; pub use initialize::*; -pub use initialize_multisig::*; pub use luts::*; pub use mark_outbox_item_as_released::*; pub use redeem::*; pub use release_inbound::*; -pub use release_inbound_multisig::*; pub use transfer::*; 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 d56a51e70..603cbf4ab 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 @@ -1,3 +1,5 @@ +use std::ops::{Deref, DerefMut}; + use anchor_lang::prelude::*; use anchor_spl::token_interface; use ntt_messages::mode::Mode; @@ -66,6 +68,34 @@ pub struct ReleaseInboundMint<'info> { common: ReleaseInbound<'info>, } +impl<'info> Deref for ReleaseInboundMint<'info> { + type Target = ReleaseInbound<'info>; + + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl Deref for ReleaseInboundMintBumps { + type Target = ReleaseInboundBumps; + + fn deref(&self) -> &Self::Target { + &self.common + } +} + +impl<'info> DerefMut for ReleaseInboundMint<'info> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.common + } +} + +#[derive(Accounts)] +pub struct ReleaseInboundMintDefault<'info> { + #[account(constraint = common.mint.mint_authority.unwrap() == common.token_authority.key())] + common: 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 @@ -74,23 +104,9 @@ pub struct ReleaseInboundMint<'info> { /// together with [`crate::instructions::redeem`] in a transaction, so that the minting /// is attempted optimistically. pub fn release_inbound_mint<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintDefault<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { - let inbox_item = &mut ctx.accounts.common.inbox_item; - - let released = inbox_item.try_release()?; - - if !released { - if args.revert_on_delay { - return Err(NTTError::CantReleaseYet.into()); - } else { - return Ok(()); - } - } - - assert!(inbox_item.release_status == ReleaseStatus::Released); - // 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 @@ -120,7 +136,7 @@ pub fn release_inbound_mint<'info>( &[ctx.bumps.common.token_authority], ]], ), - inbox_item.amount, + ctx.accounts.common.inbox_item.amount, )?; // Step 2: transfer the tokens from the custody account to the recipient @@ -131,13 +147,100 @@ pub fn release_inbound_mint<'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.inbox_item.amount, + ctx.accounts.common.mint.decimals, + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + )?; + + release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay) +} + +#[derive(Accounts)] +pub struct ReleaseInboundMintMultisig<'info> { + #[account(constraint = common.mint.mint_authority.unwrap() == multisig.key())] + common: ReleaseInboundMint<'info>, + + /// CHECK: multisig account should be mint authority + pub multisig: UncheckedAccount<'info>, +} + +pub fn release_inbound_mint_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>, + args: ReleaseInboundArgs, +) -> Result<()> { + // 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). + + // Step 1: mint tokens to the custody account + let ix = 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()], + ctx.accounts.common.inbox_item.amount, + )?; + solana_program::program::invoke_signed( + &ix, + &[ + 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(), + ], + &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]], + )?; + + // 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, + ctx.accounts.common.inbox_item.amount, ctx.accounts.common.mint.decimals, &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], )?; + + release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay) +} + +fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Result<()> { + let released = inbox_item.try_release()?; + + if !released { + if revert_on_delay { + return Err(NTTError::CantReleaseYet.into()); + } else { + return Ok(()); + } + } + + assert!(inbox_item.release_status == ReleaseStatus::Released); + Ok(()) } diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 8c60132df..a0e37cd1c 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -71,13 +71,13 @@ pub mod example_native_token_transfers { use super::*; - pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { + pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { instructions::initialize(ctx, args) } pub fn initialize_multisig( ctx: Context, - args: InitializeMultisigArgs, + args: InitializeArgs, ) -> Result<()> { instructions::initialize_multisig(ctx, args) } @@ -109,24 +109,24 @@ pub mod example_native_token_transfers { } pub fn release_inbound_mint<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintDefault<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { instructions::release_inbound_mint(ctx, args) } - pub fn release_inbound_unlock<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>, + pub fn release_inbound_mint_multisig<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { - instructions::release_inbound_unlock(ctx, args) + instructions::release_inbound_mint_multisig(ctx, args) } - pub fn release_inbound_multisig_mint<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>, - args: ReleaseInboundMultisigArgs, + pub fn release_inbound_unlock<'info>( + ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>, + args: ReleaseInboundArgs, ) -> Result<()> { - instructions::release_inbound_multisig_mint(ctx, args) + instructions::release_inbound_unlock(ctx, args) } pub fn transfer_ownership(ctx: Context) -> Result<()> { From 6ed10a7fdc89ec95b7a1e297b301595264792ca5 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 11 Dec 2024 21:18:56 -0500 Subject: [PATCH 04/19] solana: Remove unused ixs --- .../src/instructions/initialize_multisig.rs | 127 --------------- .../instructions/release_inbound_multisig.rs | 152 ------------------ 2 files changed, 279 deletions(-) delete mode 100644 solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs delete mode 100644 solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs diff --git a/solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs b/solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs deleted file mode 100644 index 3616fad00..000000000 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize_multisig.rs +++ /dev/null @@ -1,127 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::{associated_token::AssociatedToken, token_interface}; -use ntt_messages::{chain_id::ChainId, mode::Mode}; -use wormhole_solana_utils::cpi::bpf_loader_upgradeable::BpfLoaderUpgradeable; - -#[cfg(feature = "idl-build")] -use crate::messages::Hack; - -use crate::{ - bitmap::Bitmap, - error::NTTError, - queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState}, -}; - -#[derive(Accounts)] -#[instruction(args: InitializeMultisigArgs)] -pub struct InitializeMultisig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - - #[account(address = program_data.upgrade_authority_address.unwrap_or_default())] - pub deployer: Signer<'info>, - - #[account( - seeds = [crate::ID.as_ref()], - bump, - seeds::program = bpf_loader_upgradeable_program, - )] - program_data: Account<'info, ProgramData>, - - #[account( - init, - space = 8 + crate::config::Config::INIT_SPACE, - payer = payer, - seeds = [crate::config::Config::SEED_PREFIX], - bump - )] - pub config: Box>, - - #[account( - constraint = - args.mode == Mode::Locking - || mint.mint_authority.unwrap() == multisig.key() - @ NTTError::InvalidMintAuthority, - )] - pub mint: Box>, - - #[account( - init, - payer = payer, - space = 8 + OutboxRateLimit::INIT_SPACE, - seeds = [OutboxRateLimit::SEED_PREFIX], - bump, - )] - pub rate_limit: Account<'info, OutboxRateLimit>, - - #[account()] - /// CHECK: multisig is mint authority - pub multisig: UncheckedAccount<'info>, - - #[account( - seeds = [crate::TOKEN_AUTHORITY_SEED], - bump, - )] - /// CHECK: [`token_authority`] is checked against the custody account and the [`mint`]'s mint_authority - /// 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. - pub token_authority: AccountInfo<'info>, - - #[account( - init_if_needed, - payer = payer, - associated_token::mint = mint, - associated_token::authority = token_authority, - associated_token::token_program = token_program, - )] - /// The custody account that holds tokens in locking mode and temporarily - /// holds tokens in burning mode. - /// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`] - /// function if the token account has already been created. - pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, - - /// CHECK: checked to be the appropriate token program when initialising the - /// associated token account for the given mint. - pub token_program: Interface<'info, token_interface::TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, - bpf_loader_upgradeable_program: Program<'info, BpfLoaderUpgradeable>, - - system_program: Program<'info, System>, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct InitializeMultisigArgs { - pub chain_id: u16, - pub limit: u64, - pub mode: ntt_messages::mode::Mode, -} - -pub fn initialize_multisig( - ctx: Context, - args: InitializeMultisigArgs, -) -> Result<()> { - ctx.accounts.config.set_inner(crate::config::Config { - bump: ctx.bumps.config, - mint: ctx.accounts.mint.key(), - token_program: ctx.accounts.token_program.key(), - mode: args.mode, - chain_id: ChainId { id: args.chain_id }, - owner: ctx.accounts.deployer.key(), - pending_owner: None, - paused: false, - next_transceiver_id: 0, - // NOTE: can't be changed for now - threshold: 1, - enabled_transceivers: Bitmap::new(), - custody: ctx.accounts.custody.key(), - }); - - ctx.accounts.rate_limit.set_inner(OutboxRateLimit { - rate_limit: RateLimitState::new(args.limit), - }); - - Ok(()) -} diff --git a/solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs b/solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs deleted file mode 100644 index fda617706..000000000 --- a/solana/programs/example-native-token-transfers/src/instructions/release_inbound_multisig.rs +++ /dev/null @@ -1,152 +0,0 @@ -use anchor_lang::prelude::*; -use anchor_spl::token_interface; -use ntt_messages::mode::Mode; -use spl_token_2022::onchain; - -use crate::{ - config::*, - error::NTTError, - queue::inbox::{InboxItem, ReleaseStatus}, -}; - -#[derive(Accounts)] -pub struct ReleaseInboundMultisig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - - pub config: NotPausedConfig<'info>, - - #[account(mut)] - pub inbox_item: Account<'info, InboxItem>, - - #[account( - mut, - associated_token::authority = inbox_item.recipient_address, - associated_token::mint = mint, - associated_token::token_program = token_program, - )] - pub recipient: InterfaceAccount<'info, token_interface::TokenAccount>, - - /// CHECK: multisig account should be mint authority - #[account(constraint = mint.mint_authority.unwrap() == multisig.key())] - pub multisig: UncheckedAccount<'info>, - - #[account( - seeds = [crate::TOKEN_AUTHORITY_SEED], - bump, - )] - /// CHECK The seeds constraint ensures that this is the correct address - pub token_authority: UncheckedAccount<'info>, - - #[account( - mut, - address = config.mint, - )] - /// CHECK: the mint address matches the config - pub mint: InterfaceAccount<'info, token_interface::Mint>, - - pub token_program: Interface<'info, token_interface::TokenInterface>, - - /// CHECK: the token program checks if this indeed the right authority for the mint - #[account( - mut, - address = config.custody - )] - pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, -} - -#[derive(AnchorDeserialize, AnchorSerialize)] -pub struct ReleaseInboundMultisigArgs { - pub revert_on_delay: bool, -} - -// Burn/mint - -#[derive(Accounts)] -pub struct ReleaseInboundMultisigMint<'info> { - #[account( - constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, - )] - common: ReleaseInboundMultisig<'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 -/// 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 -/// is attempted optimistically. -pub fn release_inbound_multisig_mint<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMultisigMint<'info>>, - args: ReleaseInboundMultisigArgs, -) -> Result<()> { - let inbox_item = &mut ctx.accounts.common.inbox_item; - - let released = inbox_item.try_release()?; - - if !released { - if args.revert_on_delay { - return Err(NTTError::CantReleaseYet.into()); - } else { - return Ok(()); - } - } - - assert!(inbox_item.release_status == ReleaseStatus::Released); - - // 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). - - // Step 1: mint tokens to the custody account - let ix = spl_token_2022::instruction::mint_to( - &ctx.accounts.common.token_program.key(), - &ctx.accounts.common.mint.key(), - &ctx.accounts.common.custody.key(), - &ctx.accounts.common.multisig.key(), - &[&ctx.accounts.common.token_authority.key()], - inbox_item.amount, - )?; - solana_program::program::invoke_signed( - &ix, - &[ - ctx.accounts.common.custody.to_account_info(), - ctx.accounts.common.mint.to_account_info(), - ctx.accounts.common.token_authority.to_account_info(), - ctx.accounts.common.multisig.to_account_info(), - ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - - // 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, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], - )?; - Ok(()) -} From ec8d8da992ecf30d39b8d73a231a2b1e9aea3cf5 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 11 Dec 2024 21:25:16 -0500 Subject: [PATCH 05/19] solana: Refactor out constraint from struct and function --- .../src/instructions/initialize.rs | 23 ++-- .../src/instructions/release_inbound.rs | 105 +++++++----------- .../example-native-token-transfers/src/lib.rs | 4 +- 3 files changed, 48 insertions(+), 84 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 bb628306a..ec471c065 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -13,7 +13,6 @@ use crate::{ }; #[derive(Accounts)] -#[instruction(args: InitializeArgs)] pub struct Initialize<'info> { #[account(mut)] pub payer: Signer<'info>, @@ -90,22 +89,16 @@ pub struct InitializeArgs { pub mode: ntt_messages::mode::Mode, } -#[derive(Accounts)] -#[instruction(args: InitializeArgs)] -pub struct InitializeDefault<'info> { - #[account( - constraint = - args.mode == Mode::Locking - || common.mint.mint_authority.unwrap() == common.token_authority.key() - @ NTTError::InvalidMintAuthority, - )] - pub common: Initialize<'info>, -} +pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { + if !(args.mode == Mode::Locking + || ctx.accounts.mint.mint_authority.unwrap() == ctx.accounts.token_authority.key()) + { + return Err(NTTError::InvalidMintAuthority.into()); + } -pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { initialize_config_and_rate_limit( - &mut ctx.accounts.common, - ctx.bumps.common.config, + ctx.accounts, + ctx.bumps.config, args.chain_id, args.limit, args.mode, 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 603cbf4ab..b9ecc8f6e 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 @@ -1,5 +1,3 @@ -use std::ops::{Deref, DerefMut}; - use anchor_lang::prelude::*; use anchor_spl::token_interface; use ntt_messages::mode::Mode; @@ -68,34 +66,6 @@ pub struct ReleaseInboundMint<'info> { common: ReleaseInbound<'info>, } -impl<'info> Deref for ReleaseInboundMint<'info> { - type Target = ReleaseInbound<'info>; - - fn deref(&self) -> &Self::Target { - &self.common - } -} - -impl Deref for ReleaseInboundMintBumps { - type Target = ReleaseInboundBumps; - - fn deref(&self) -> &Self::Target { - &self.common - } -} - -impl<'info> DerefMut for ReleaseInboundMint<'info> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.common - } -} - -#[derive(Accounts)] -pub struct ReleaseInboundMintDefault<'info> { - #[account(constraint = common.mint.mint_authority.unwrap() == common.token_authority.key())] - common: 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 @@ -104,9 +74,16 @@ pub struct ReleaseInboundMintDefault<'info> { /// together with [`crate::instructions::redeem`] in a transaction, so that the minting /// is attempted optimistically. pub fn release_inbound_mint<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintDefault<'info>>, + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { + if ctx.accounts.common.mint.mint_authority.unwrap() != ctx.accounts.common.token_authority.key() + { + return Err(NTTError::InvalidMintAuthority.into()); + } + + let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?; + // 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 @@ -136,7 +113,7 @@ pub fn release_inbound_mint<'info>( &[ctx.bumps.common.token_authority], ]], ), - ctx.accounts.common.inbox_item.amount, + inbox_item.amount, )?; // Step 2: transfer the tokens from the custody account to the recipient @@ -147,21 +124,23 @@ pub fn release_inbound_mint<'info>( ctx.accounts.common.recipient.to_account_info(), ctx.accounts.common.token_authority.to_account_info(), ctx.remaining_accounts, - ctx.accounts.common.inbox_item.amount, + inbox_item.amount, ctx.accounts.common.mint.decimals, &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], )?; - - release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay) + Ok(()) } #[derive(Accounts)] pub struct ReleaseInboundMintMultisig<'info> { - #[account(constraint = common.mint.mint_authority.unwrap() == multisig.key())] - common: ReleaseInboundMint<'info>, + #[account( + constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, + constraint = common.mint.mint_authority.unwrap() == multisig.key() + )] + common: ReleaseInbound<'info>, /// CHECK: multisig account should be mint authority pub multisig: UncheckedAccount<'info>, @@ -171,6 +150,8 @@ pub fn release_inbound_mint_multisig<'info>( ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintMultisig<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { + let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?; + // 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 @@ -193,7 +174,7 @@ pub fn release_inbound_mint_multisig<'info>( &ctx.accounts.common.custody.key(), &ctx.accounts.multisig.key(), &[&ctx.accounts.common.token_authority.key()], - ctx.accounts.common.inbox_item.amount, + inbox_item.amount, )?; solana_program::program::invoke_signed( &ix, @@ -217,30 +198,13 @@ pub fn release_inbound_mint_multisig<'info>( ctx.accounts.common.recipient.to_account_info(), ctx.accounts.common.token_authority.to_account_info(), ctx.remaining_accounts, - ctx.accounts.common.inbox_item.amount, + inbox_item.amount, ctx.accounts.common.mint.decimals, &[&[ crate::TOKEN_AUTHORITY_SEED, &[ctx.bumps.common.token_authority], ]], )?; - - release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay) -} - -fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Result<()> { - let released = inbox_item.try_release()?; - - if !released { - if revert_on_delay { - return Err(NTTError::CantReleaseYet.into()); - } else { - return Ok(()); - } - } - - assert!(inbox_item.release_status == ReleaseStatus::Released); - Ok(()) } @@ -250,6 +214,7 @@ fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Resu pub struct ReleaseInboundUnlock<'info> { #[account( constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode, + constraint = common.mint.mint_authority.unwrap() == common.token_authority.key() )] common: ReleaseInbound<'info>, } @@ -265,17 +230,7 @@ pub fn release_inbound_unlock<'info>( ctx: Context<'_, '_, '_, 'info, ReleaseInboundUnlock<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { - let inbox_item = &mut ctx.accounts.common.inbox_item; - - let released = inbox_item.try_release()?; - - if !released { - if args.revert_on_delay { - return Err(NTTError::CantReleaseYet.into()); - } else { - return Ok(()); - } - } + let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?; onchain::invoke_transfer_checked( &ctx.accounts.common.token_program.key(), @@ -293,3 +248,19 @@ pub fn release_inbound_unlock<'info>( )?; Ok(()) } + +fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Result<&mut InboxItem> { + let released = inbox_item.try_release()?; + + if !released { + if revert_on_delay { + return Err(NTTError::CantReleaseYet.into()); + } else { + return Ok(inbox_item); + } + } + + assert!(inbox_item.release_status == ReleaseStatus::Released); + + Ok(inbox_item) +} diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index a0e37cd1c..759c5ed27 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -71,7 +71,7 @@ pub mod example_native_token_transfers { use super::*; - pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { + pub fn initialize(ctx: Context, args: InitializeArgs) -> Result<()> { instructions::initialize(ctx, args) } @@ -109,7 +109,7 @@ pub mod example_native_token_transfers { } pub fn release_inbound_mint<'info>( - ctx: Context<'_, '_, '_, 'info, ReleaseInboundMintDefault<'info>>, + ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { instructions::release_inbound_mint(ctx, args) From a3b69285c202bd7961cee5a2006bdf18f2046852 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Wed, 11 Dec 2024 21:25:47 -0500 Subject: [PATCH 06/19] solana: Update SDK code to match program --- solana/ts/lib/ntt.ts | 40 +++++++++++++++++++++------------------- solana/ts/sdk/ntt.ts | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/solana/ts/lib/ntt.ts b/solana/ts/lib/ntt.ts index 1bbb8204d..4aed49162 100644 --- a/solana/ts/lib/ntt.ts +++ b/solana/ts/lib/ntt.ts @@ -301,23 +301,25 @@ export namespace NTT { return await program.methods .initializeMultisig({ chainId, limit: limit, mode }) .accountsStrict({ - payer: args.payer, - deployer: args.owner, - programData: programDataAddress(program.programId), - config: pdas.configAccount(), - mint: args.mint, - rateLimit: pdas.outboxRateLimitAccount(), + 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, - tokenProgram: args.tokenProgram, - tokenAuthority: pdas.tokenAuthority(), - custody: await NTT.custodyAccountAddress( - pdas, - args.mint, - args.tokenProgram - ), - bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_PROGRAM_ID, - associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID, - systemProgram: SystemProgram.programId, }) .instruction(); } @@ -661,7 +663,7 @@ export namespace NTT { // 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 createReleaseInboundMultisigMintInstruction( + export async function createReleaseInboundMintMultisigInstruction( program: Program>, config: NttBindings.Config, args: { @@ -682,7 +684,7 @@ export namespace NTT { .recipientAddress; const transferIx = await program.methods - .releaseInboundMultisigMint({ + .releaseInboundMintMultisig({ revertOnDelay: args.revertOnDelay, }) .accountsStrict({ @@ -700,8 +702,8 @@ export namespace NTT { tokenAuthority: pdas.tokenAuthority(), tokenProgram: config.tokenProgram, custody: await custodyAccountAddress(pdas, config), - multisig: args.multisig, }, + multisig: args.multisig, }) .instruction(); diff --git a/solana/ts/sdk/ntt.ts b/solana/ts/sdk/ntt.ts index e106cc774..5a4577de0 100644 --- a/solana/ts/sdk/ntt.ts +++ b/solana/ts/sdk/ntt.ts @@ -1011,7 +1011,7 @@ export class SolanaNtt ...releaseArgs, }) : multisig - ? NTT.createReleaseInboundMultisigMintInstruction( + ? NTT.createReleaseInboundMintMultisigInstruction( this.program, config, { From ab9b80cd97a0ba99ca1ca405f61d3adcfc7bfd3b Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 13 Dec 2024 14:11:23 -0500 Subject: [PATCH 07/19] solana: Remove empty `#[account()]` and comment on constraint move --- .../src/instructions/initialize.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 ec471c065..a54a352e5 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -36,7 +36,6 @@ pub struct Initialize<'info> { )] pub config: Box>, - #[account()] pub mint: Box>, #[account( @@ -90,6 +89,8 @@ 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::Locking || ctx.accounts.mint.mint_authority.unwrap() == ctx.accounts.token_authority.key()) { @@ -116,7 +117,6 @@ pub struct InitializeMultisig<'info> { )] pub common: Initialize<'info>, - #[account()] /// CHECK: multisig is mint authority pub multisig: UncheckedAccount<'info>, } From fe0e201bf824f8188408fe02cb211a9a994e5a3a Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 13 Dec 2024 14:14:07 -0500 Subject: [PATCH 08/19] solana: Refactor `token_authority_sig`, simplify `release_inbox_item` logic, move constraint back to struct --- .../src/instructions/release_inbound.rs | 72 ++++++++----------- 1 file changed, 30 insertions(+), 42 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 b9ecc8f6e..4023e303f 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 @@ -62,6 +62,7 @@ pub struct ReleaseInboundArgs { pub struct ReleaseInboundMint<'info> { #[account( constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, + constraint = common.mint.mint_authority.unwrap() == common.token_authority.key() )] common: ReleaseInbound<'info>, } @@ -77,11 +78,6 @@ pub fn release_inbound_mint<'info>( ctx: Context<'_, '_, '_, 'info, ReleaseInboundMint<'info>>, args: ReleaseInboundArgs, ) -> Result<()> { - if ctx.accounts.common.mint.mint_authority.unwrap() != ctx.accounts.common.token_authority.key() - { - return Err(NTTError::InvalidMintAuthority.into()); - } - let inbox_item = release_inbox_item(&mut ctx.accounts.common.inbox_item, args.revert_on_delay)?; // NOTE: minting tokens is a two-step process: @@ -99,6 +95,11 @@ pub fn release_inbound_mint<'info>( // The [`transfer_burn`] function operates in a similar way // (transfer to custody from sender, *then* burn). + let token_authority_sig: &[&[&[u8]]] = &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]]; + // Step 1: mint tokens to the custody account token_interface::mint_to( CpiContext::new_with_signer( @@ -108,10 +109,7 @@ pub fn release_inbound_mint<'info>( to: ctx.accounts.common.custody.to_account_info(), authority: ctx.accounts.common.token_authority.to_account_info(), }, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], + token_authority_sig, ), inbox_item.amount, )?; @@ -126,10 +124,7 @@ pub fn release_inbound_mint<'info>( ctx.remaining_accounts, inbox_item.amount, ctx.accounts.common.mint.decimals, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], + token_authority_sig, )?; Ok(()) } @@ -167,27 +162,28 @@ pub fn release_inbound_mint_multisig<'info>( // The [`transfer_burn`] function operates in a similar way // (transfer to custody from sender, *then* burn). + let token_authority_sig: &[&[&[u8]]] = &[&[ + crate::TOKEN_AUTHORITY_SEED, + &[ctx.bumps.common.token_authority], + ]]; + // Step 1: mint tokens to the custody account - let ix = 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, - )?; solana_program::program::invoke_signed( - &ix, + &spl_token_2022::instruction::mint_to( + &ctx.accounts.common.token_program.key(), + &ctx.accounts.common.mint.key(), + &ctx.accounts.common.custody.key(), + &ctx.accounts.multisig.key(), + &[&ctx.accounts.common.token_authority.key()], + inbox_item.amount, + )?, &[ ctx.accounts.common.custody.to_account_info(), ctx.accounts.common.mint.to_account_info(), ctx.accounts.common.token_authority.to_account_info(), ctx.accounts.multisig.to_account_info(), ], - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], + token_authority_sig, )?; // Step 2: transfer the tokens from the custody account to the recipient @@ -200,10 +196,7 @@ pub fn release_inbound_mint_multisig<'info>( ctx.remaining_accounts, inbox_item.amount, ctx.accounts.common.mint.decimals, - &[&[ - crate::TOKEN_AUTHORITY_SEED, - &[ctx.bumps.common.token_authority], - ]], + token_authority_sig, )?; Ok(()) } @@ -250,17 +243,12 @@ pub fn release_inbound_unlock<'info>( } fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Result<&mut InboxItem> { - let released = inbox_item.try_release()?; - - if !released { - if revert_on_delay { - return Err(NTTError::CantReleaseYet.into()); - } else { - return Ok(inbox_item); - } + if inbox_item.try_release()? { + assert!(inbox_item.release_status == ReleaseStatus::Released); + Ok(inbox_item) + } else if revert_on_delay { + Err(NTTError::CantReleaseYet.into()) + } else { + Ok(inbox_item) } - - assert!(inbox_item.release_status == ReleaseStatus::Released); - - Ok(inbox_item) } From b8519061451417dbc0ed3cc882ef309f7f7c3671 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 13 Dec 2024 14:15:26 -0500 Subject: [PATCH 09/19] solana: Remove explicit commitment --- solana/tests/anchor.test.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index 6f4e005e1..a92627910 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -173,14 +173,10 @@ describe("example-native-token-transfers", () => { transaction.feePayer = payer.publicKey; transaction.recentBlockhash = blockhash; - await anchor.web3.sendAndConfirmTransaction( - connection, - transaction, - [payer, mint], - { - commitment: "confirmed", - } - ); + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + payer, + mint, + ]); tokenAccount = await spl.createAssociatedTokenAccount( connection, @@ -307,14 +303,9 @@ describe("example-native-token-transfers", () => { transaction.recentBlockhash = blockhash; transaction.sign(payer); - await anchor.web3.sendAndConfirmTransaction( - connection, - transaction, - [payer], - { - commitment: "confirmed", - } - ); + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + payer, + ]); }); test("Can send tokens", async () => { From 1169a979ba2c5946facc3cef41959f0a28cd1bb7 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Sat, 14 Dec 2024 01:52:30 -0500 Subject: [PATCH 10/19] solana: Add SPL Multisig wrapper for `InterfaceAccount` --- .../example-native-token-transfers/src/lib.rs | 1 + .../src/spl_multisig.rs | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 solana/programs/example-native-token-transfers/src/spl_multisig.rs diff --git a/solana/programs/example-native-token-transfers/src/lib.rs b/solana/programs/example-native-token-transfers/src/lib.rs index 759c5ed27..7b480b498 100644 --- a/solana/programs/example-native-token-transfers/src/lib.rs +++ b/solana/programs/example-native-token-transfers/src/lib.rs @@ -21,6 +21,7 @@ pub mod peer; pub mod pending_token_authority; pub mod queue; pub mod registered_transceiver; +pub mod spl_multisig; pub mod transceivers; pub mod transfer; diff --git a/solana/programs/example-native-token-transfers/src/spl_multisig.rs b/solana/programs/example-native-token-transfers/src/spl_multisig.rs new file mode 100644 index 000000000..61e77b675 --- /dev/null +++ b/solana/programs/example-native-token-transfers/src/spl_multisig.rs @@ -0,0 +1,37 @@ +use anchor_lang::{prelude::*, solana_program::program_pack::Pack, Ids, Owners}; +use anchor_spl::token_interface::TokenInterface; +use std::ops::Deref; + +/// Anchor does not have a SPL Multisig wrapper as a part of the token interface: +/// https://docs.rs/anchor-spl/0.29.0/src/anchor_spl/token_interface.rs.html +/// Thus, we have to write our own wrapper to use with `InterfaceAccount` + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct SplMultisig(spl_token_2022::state::Multisig); + +impl AccountDeserialize for SplMultisig { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { + spl_token_2022::state::Multisig::unpack(buf) + .map(|t| SplMultisig(t)) + .map_err(Into::into) + } +} + +impl AccountSerialize for SplMultisig {} + +impl Owners for SplMultisig { + fn owners() -> &'static [Pubkey] { + TokenInterface::ids() + } +} + +impl Deref for SplMultisig { + type Target = spl_token_2022::state::Multisig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "idl-build")] +impl anchor_lang::IdlBuild for SplMultisig {} From 0b29805ed1a0ce174a2a394aa96fe6de34b5b6ff Mon Sep 17 00:00:00 2001 From: nvsriram Date: Sat, 14 Dec 2024 01:54:29 -0500 Subject: [PATCH 11/19] solana: Validate token_authority is multisig signer --- .../src/error.rs | 2 + .../src/instructions/initialize.rs | 9 +- .../src/instructions/release_inbound.rs | 9 +- .../json/example_native_token_transfers.json | 208 ++++----- .../ts/example_native_token_transfers.ts | 416 ++++++++---------- 5 files changed, 292 insertions(+), 352 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/error.rs b/solana/programs/example-native-token-transfers/src/error.rs index 5b3efe3cf..2156b584a 100644 --- a/solana/programs/example-native-token-transfers/src/error.rs +++ b/solana/programs/example-native-token-transfers/src/error.rs @@ -59,6 +59,8 @@ pub enum NTTError { InvalidPendingTokenAuthority, #[msg("IncorrectRentPayer")] IncorrectRentPayer, + #[msg("InvalidMultisig")] + InvalidMultisig, } impl From for NTTError { 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 a54a352e5..77e698696 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -10,6 +10,7 @@ use crate::{ bitmap::Bitmap, error::NTTError, queue::{outbox::OutboxRateLimit, rate_limit::RateLimitState}, + spl_multisig::SplMultisig, }; #[derive(Accounts)] @@ -117,8 +118,12 @@ pub struct InitializeMultisig<'info> { )] pub common: Initialize<'info>, - /// CHECK: multisig is mint authority - pub multisig: UncheckedAccount<'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<()> { 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 4023e303f..3a68cef9d 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 @@ -7,6 +7,7 @@ use crate::{ config::*, error::NTTError, queue::inbox::{InboxItem, ReleaseStatus}, + spl_multisig::SplMultisig, }; #[derive(Accounts)] @@ -137,8 +138,12 @@ pub struct ReleaseInboundMintMultisig<'info> { )] common: ReleaseInbound<'info>, - /// CHECK: multisig account should be mint authority - pub multisig: UncheckedAccount<'info>, + #[account( + constraint = + multisig.m == 1 && multisig.signers.contains(&common.token_authority.key()) + @ NTTError::InvalidMultisig, + )] + pub multisig: InterfaceAccount<'info, SplMultisig>, } pub fn release_inbound_mint_multisig<'info>( 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 4601c5d57..829fb92ee 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 @@ -94,82 +94,87 @@ "name": "initializeMultisig", "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": "multisig", - "isMut": false, - "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": "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": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "bpfLoaderUpgradeableProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", + "name": "multisig", "isMut": false, "isSigner": false } @@ -178,7 +183,7 @@ { "name": "args", "type": { - "defined": "InitializeMultisigArgs" + "defined": "InitializeArgs" } } ] @@ -644,7 +649,7 @@ ] }, { - "name": "releaseInboundUnlock", + "name": "releaseInboundMintMultisig", "accounts": [ { "name": "common", @@ -698,6 +703,11 @@ "isSigner": false } ] + }, + { + "name": "multisig", + "isMut": false, + "isSigner": false } ], "args": [ @@ -710,7 +720,7 @@ ] }, { - "name": "releaseInboundMultisigMint", + "name": "releaseInboundUnlock", "accounts": [ { "name": "common", @@ -740,11 +750,6 @@ "isMut": true, "isSigner": false }, - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, { "name": "tokenAuthority", "isMut": false, @@ -775,7 +780,7 @@ { "name": "args", "type": { - "defined": "ReleaseInboundMultisigArgs" + "defined": "ReleaseInboundArgs" } } ] @@ -2084,28 +2089,6 @@ ] } }, - { - "name": "InitializeMultisigArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "chainId", - "type": "u16" - }, - { - "name": "limit", - "type": "u64" - }, - { - "name": "mode", - "type": { - "defined": "Mode" - } - } - ] - } - }, { "name": "RedeemArgs", "type": { @@ -2125,18 +2108,6 @@ ] } }, - { - "name": "ReleaseInboundMultisigArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "revertOnDelay", - "type": "bool" - } - ] - } - }, { "name": "TransferArgs", "type": { @@ -2477,6 +2448,11 @@ "code": 6026, "name": "IncorrectRentPayer", "msg": "IncorrectRentPayer" + }, + { + "code": 6027, + "name": "InvalidMultisig", + "msg": "InvalidMultisig" } ] } 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 268aa6589..73cb4d0b5 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 @@ -94,82 +94,87 @@ export type ExampleNativeTokenTransfers = { "name": "initializeMultisig", "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": "multisig", - "isMut": false, - "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": "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": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "bpfLoaderUpgradeableProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", + "name": "multisig", "isMut": false, "isSigner": false } @@ -178,7 +183,7 @@ export type ExampleNativeTokenTransfers = { { "name": "args", "type": { - "defined": "InitializeMultisigArgs" + "defined": "InitializeArgs" } } ] @@ -644,7 +649,7 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "releaseInboundUnlock", + "name": "releaseInboundMintMultisig", "accounts": [ { "name": "common", @@ -698,6 +703,11 @@ export type ExampleNativeTokenTransfers = { "isSigner": false } ] + }, + { + "name": "multisig", + "isMut": false, + "isSigner": false } ], "args": [ @@ -710,7 +720,7 @@ export type ExampleNativeTokenTransfers = { ] }, { - "name": "releaseInboundMultisigMint", + "name": "releaseInboundUnlock", "accounts": [ { "name": "common", @@ -740,11 +750,6 @@ export type ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false }, - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, { "name": "tokenAuthority", "isMut": false, @@ -775,7 +780,7 @@ export type ExampleNativeTokenTransfers = { { "name": "args", "type": { - "defined": "ReleaseInboundMultisigArgs" + "defined": "ReleaseInboundArgs" } } ] @@ -2084,28 +2089,6 @@ export type ExampleNativeTokenTransfers = { ] } }, - { - "name": "InitializeMultisigArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "chainId", - "type": "u16" - }, - { - "name": "limit", - "type": "u64" - }, - { - "name": "mode", - "type": { - "defined": "Mode" - } - } - ] - } - }, { "name": "RedeemArgs", "type": { @@ -2125,18 +2108,6 @@ export type ExampleNativeTokenTransfers = { ] } }, - { - "name": "ReleaseInboundMultisigArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "revertOnDelay", - "type": "bool" - } - ] - } - }, { "name": "TransferArgs", "type": { @@ -2477,6 +2448,11 @@ export type ExampleNativeTokenTransfers = { "code": 6026, "name": "IncorrectRentPayer", "msg": "IncorrectRentPayer" + }, + { + "code": 6027, + "name": "InvalidMultisig", + "msg": "InvalidMultisig" } ] } @@ -2576,82 +2552,87 @@ export const IDL: ExampleNativeTokenTransfers = { "name": "initializeMultisig", "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": "multisig", - "isMut": false, - "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": "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": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "bpfLoaderUpgradeableProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", + "name": "multisig", "isMut": false, "isSigner": false } @@ -2660,7 +2641,7 @@ export const IDL: ExampleNativeTokenTransfers = { { "name": "args", "type": { - "defined": "InitializeMultisigArgs" + "defined": "InitializeArgs" } } ] @@ -3126,7 +3107,7 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "releaseInboundUnlock", + "name": "releaseInboundMintMultisig", "accounts": [ { "name": "common", @@ -3180,6 +3161,11 @@ export const IDL: ExampleNativeTokenTransfers = { "isSigner": false } ] + }, + { + "name": "multisig", + "isMut": false, + "isSigner": false } ], "args": [ @@ -3192,7 +3178,7 @@ export const IDL: ExampleNativeTokenTransfers = { ] }, { - "name": "releaseInboundMultisigMint", + "name": "releaseInboundUnlock", "accounts": [ { "name": "common", @@ -3222,11 +3208,6 @@ export const IDL: ExampleNativeTokenTransfers = { "isMut": true, "isSigner": false }, - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, { "name": "tokenAuthority", "isMut": false, @@ -3257,7 +3238,7 @@ export const IDL: ExampleNativeTokenTransfers = { { "name": "args", "type": { - "defined": "ReleaseInboundMultisigArgs" + "defined": "ReleaseInboundArgs" } } ] @@ -4566,28 +4547,6 @@ export const IDL: ExampleNativeTokenTransfers = { ] } }, - { - "name": "InitializeMultisigArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "chainId", - "type": "u16" - }, - { - "name": "limit", - "type": "u64" - }, - { - "name": "mode", - "type": { - "defined": "Mode" - } - } - ] - } - }, { "name": "RedeemArgs", "type": { @@ -4607,18 +4566,6 @@ export const IDL: ExampleNativeTokenTransfers = { ] } }, - { - "name": "ReleaseInboundMultisigArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "revertOnDelay", - "type": "bool" - } - ] - } - }, { "name": "TransferArgs", "type": { @@ -4959,6 +4906,11 @@ export const IDL: ExampleNativeTokenTransfers = { "code": 6026, "name": "IncorrectRentPayer", "msg": "IncorrectRentPayer" + }, + { + "code": 6027, + "name": "InvalidMultisig", + "msg": "InvalidMultisig" } ] } From da03ac1d79626a300ec9a13eeb4b2870e5022692 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Sat, 14 Dec 2024 02:38:53 -0500 Subject: [PATCH 12/19] nit: Fix lints --- .../src/instructions/initialize.rs | 4 ++-- .../example-native-token-transfers/src/spl_multisig.rs | 2 +- 2 files changed, 3 insertions(+), 3 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 77e698696..963895a6c 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -70,7 +70,7 @@ pub struct Initialize<'info> { /// The custody account that holds tokens in locking mode and temporarily /// holds tokens in burning mode. /// CHECK: Use init_if_needed here to prevent a denial-of-service of the [`initialize`] - /// function if the token account has already been created. + /// function if the token account has already been created. pub custody: InterfaceAccount<'info, token_interface::TokenAccount>, /// CHECK: checked to be the appropriate token program when initialising the @@ -121,7 +121,7 @@ pub struct InitializeMultisig<'info> { #[account( constraint = multisig.m == 1 && multisig.signers.contains(&common.token_authority.key()) - @ NTTError::InvalidMultisig, + @ NTTError::InvalidMultisig, )] pub multisig: InterfaceAccount<'info, SplMultisig>, } diff --git a/solana/programs/example-native-token-transfers/src/spl_multisig.rs b/solana/programs/example-native-token-transfers/src/spl_multisig.rs index 61e77b675..0b6c7c076 100644 --- a/solana/programs/example-native-token-transfers/src/spl_multisig.rs +++ b/solana/programs/example-native-token-transfers/src/spl_multisig.rs @@ -12,7 +12,7 @@ pub struct SplMultisig(spl_token_2022::state::Multisig); impl AccountDeserialize for SplMultisig { fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { spl_token_2022::state::Multisig::unpack(buf) - .map(|t| SplMultisig(t)) + .map(SplMultisig) .map_err(Into::into) } } From 915e268b4c1e4ea22b119d052a3c4a8ea3a15d99 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 15:03:09 -0500 Subject: [PATCH 13/19] solana: Convert check to direct expression --- .../src/instructions/initialize.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 963895a6c..6f6122621 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -92,8 +92,8 @@ 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::Locking - || ctx.accounts.mint.mint_authority.unwrap() == ctx.accounts.token_authority.key()) + if args.mode == Mode::Burning + && ctx.accounts.mint.mint_authority.unwrap() != ctx.accounts.token_authority.key() { return Err(NTTError::InvalidMintAuthority.into()); } From e7d2d9ebc7ad643be62fedfd51893165b14e6541 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 15:04:13 -0500 Subject: [PATCH 14/19] solana: Remove redundant "early check" --- .../src/instructions/release_inbound.rs | 3 --- 1 file changed, 3 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 3a68cef9d..f08181503 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 @@ -63,7 +63,6 @@ pub struct ReleaseInboundArgs { pub struct ReleaseInboundMint<'info> { #[account( constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, - constraint = common.mint.mint_authority.unwrap() == common.token_authority.key() )] common: ReleaseInbound<'info>, } @@ -134,7 +133,6 @@ pub fn release_inbound_mint<'info>( pub struct ReleaseInboundMintMultisig<'info> { #[account( constraint = common.config.mode == Mode::Burning @ NTTError::InvalidMode, - constraint = common.mint.mint_authority.unwrap() == multisig.key() )] common: ReleaseInbound<'info>, @@ -212,7 +210,6 @@ pub fn release_inbound_mint_multisig<'info>( pub struct ReleaseInboundUnlock<'info> { #[account( constraint = common.config.mode == Mode::Locking @ NTTError::InvalidMode, - constraint = common.mint.mint_authority.unwrap() == common.token_authority.key() )] common: ReleaseInbound<'info>, } From bdc7f478c74492357433e30af2e63a9782e75d9a Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 15:04:57 -0500 Subject: [PATCH 15/19] solana: Move `TOKEN_PROGRAM` const next to other consts --- solana/tests/anchor.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/solana/tests/anchor.test.ts b/solana/tests/anchor.test.ts index a92627910..a582890b5 100644 --- a/solana/tests/anchor.test.ts +++ b/solana/tests/anchor.test.ts @@ -32,6 +32,7 @@ import { SolanaNtt } from "../ts/sdk/index.js"; const solanaRootDir = `${__dirname}/../`; const VERSION: IdlVersion = "3.0.0"; +const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID; const GUARDIAN_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"; const CORE_BRIDGE_ADDRESS = contracts.coreBridge("Mainnet", "Solana"); @@ -123,8 +124,6 @@ const nttTransceivers = { ), }; -const TOKEN_PROGRAM = spl.TOKEN_2022_PROGRAM_ID; - describe("example-native-token-transfers", () => { let ntt: SolanaNtt<"Devnet", "Solana">; let signer: Signer; From 484d73cd19fb3961dcfb6127aad8626b5620cfa7 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 15:08:07 -0500 Subject: [PATCH 16/19] solana: Inline return --- .../example-native-token-transfers/src/spl_multisig.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/solana/programs/example-native-token-transfers/src/spl_multisig.rs b/solana/programs/example-native-token-transfers/src/spl_multisig.rs index 0b6c7c076..9d5cdb918 100644 --- a/solana/programs/example-native-token-transfers/src/spl_multisig.rs +++ b/solana/programs/example-native-token-transfers/src/spl_multisig.rs @@ -11,9 +11,7 @@ pub struct SplMultisig(spl_token_2022::state::Multisig); impl AccountDeserialize for SplMultisig { fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result { - spl_token_2022::state::Multisig::unpack(buf) - .map(SplMultisig) - .map_err(Into::into) + Ok(SplMultisig(spl_token_2022::state::Multisig::unpack(buf)?)) } } From 46e413f779b0bde3ceb623079e617f6cbdf1c60c Mon Sep 17 00:00:00 2001 From: nvsriram Date: Mon, 23 Dec 2024 15:08:59 -0500 Subject: [PATCH 17/19] nit: Remove extra space in IDL --- .../ts/idl/3_0_0/json/example_native_token_transfers.json | 4 ++-- solana/ts/idl/3_0_0/ts/example_native_token_transfers.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 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 829fb92ee..ea99bdc9e 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 @@ -54,7 +54,7 @@ "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." + "function if the token account has already been created." ] }, { @@ -145,7 +145,7 @@ "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." + "function if the token account has already been created." ] }, { 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 73cb4d0b5..30ccc2364 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 @@ -54,7 +54,7 @@ export type ExampleNativeTokenTransfers = { "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." + "function if the token account has already been created." ] }, { @@ -145,7 +145,7 @@ export type ExampleNativeTokenTransfers = { "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." + "function if the token account has already been created." ] }, { @@ -2512,7 +2512,7 @@ export const IDL: ExampleNativeTokenTransfers = { "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." + "function if the token account has already been created." ] }, { @@ -2603,7 +2603,7 @@ export const IDL: ExampleNativeTokenTransfers = { "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." + "function if the token account has already been created." ] }, { From bb9d83fbf337fc0e06567741d27ddfa7de94fb5b Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 17 Jan 2025 19:43:21 +0400 Subject: [PATCH 18/19] solana: Add note on unconstrained mint account --- .../src/instructions/initialize.rs | 2 ++ 1 file changed, 2 insertions(+) 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 6f6122621..dfa889bb9 100644 --- a/solana/programs/example-native-token-transfers/src/instructions/initialize.rs +++ b/solana/programs/example-native-token-transfers/src/instructions/initialize.rs @@ -37,6 +37,8 @@ pub struct Initialize<'info> { )] pub config: Box>, + // NOTE: this account is unconstrained and is the responsibility of the + // handler to constrain it pub mint: Box>, #[account( From f49e1f41470a8737c032cabd921619ffe91f4744 Mon Sep 17 00:00:00 2001 From: nvsriram Date: Fri, 17 Jan 2025 19:54:14 +0400 Subject: [PATCH 19/19] solana: Update `release_inbox_item` to return `Result>` --- .../src/instructions/release_inbound.rs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 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 f08181503..623c3b509 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 @@ -79,6 +79,11 @@ pub fn release_inbound_mint<'info>( args: ReleaseInboundArgs, ) -> 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 @@ -149,6 +154,11 @@ pub fn release_inbound_mint_multisig<'info>( args: ReleaseInboundArgs, ) -> 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 @@ -226,6 +236,11 @@ pub fn release_inbound_unlock<'info>( args: ReleaseInboundArgs, ) -> 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); onchain::invoke_transfer_checked( &ctx.accounts.common.token_program.key(), @@ -243,14 +258,15 @@ pub fn release_inbound_unlock<'info>( )?; Ok(()) } - -fn release_inbox_item(inbox_item: &mut InboxItem, revert_on_delay: bool) -> Result<&mut InboxItem> { +fn release_inbox_item( + inbox_item: &mut InboxItem, + revert_on_delay: bool, +) -> Result> { if inbox_item.try_release()? { - assert!(inbox_item.release_status == ReleaseStatus::Released); - Ok(inbox_item) + Ok(Some(inbox_item)) } else if revert_on_delay { Err(NTTError::CantReleaseYet.into()) } else { - Ok(inbox_item) + Ok(None) } }