From 63578825c12e5adbb7a82ac2e9c7d932d82d9940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20=E5=88=A9=E8=BF=AA=E6=81=A9?= Date: Tue, 14 Oct 2025 04:26:13 +0800 Subject: [PATCH 1/2] WIP: Recover nested tokens from ATA of ATA --- Cargo.lock | 95 ++++++++++++++++--- programs/launchpad/Cargo.toml | 1 + programs/launchpad/src/instructions/mod.rs | 2 + .../src/instructions/recover_nested_tokens.rs | 63 ++++++++++++ programs/launchpad/src/lib.rs | 4 + .../src/state/associated_token_program.rs | 57 +++++++++++ programs/launchpad/src/state/mod.rs | 2 + 7 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 programs/launchpad/src/instructions/recover_nested_tokens.rs create mode 100644 programs/launchpad/src/state/associated_token_program.rs diff --git a/Cargo.lock b/Cargo.lock index cae11fcb..e7655f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aead" @@ -212,7 +212,7 @@ dependencies = [ "solana-program", "spl-associated-token-account", "spl-token", - "spl-token-2022", + "spl-token-2022 0.9.0", ] [[package]] @@ -1152,6 +1152,7 @@ dependencies = [ "price_based_performance_package", "solana-program", "solana-security-txt", + "spl-associated-token-account", "spl-memo", "spl-token", "squads-multisig-program", @@ -1355,11 +1356,12 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70bf6736f74634d299d00086f02986875b3c2d924781a6a2cb6c201e73da0ceb" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ - "num_enum_derive 0.7.0", + "num_enum_derive 0.7.4", + "rustversion", ] [[package]] @@ -1376,9 +1378,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", @@ -2069,9 +2071,9 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385e31c29981488f2820b2022d8e731aae3b02e6e18e2fd854e4c9a94dc44fc3" +checksum = "992d9c64c2564cc8f63a4b508bf3ebcdf2254b0429b13cd1d31adb6162432a5f" dependencies = [ "assert_matches", "borsh 0.10.4", @@ -2079,7 +2081,7 @@ dependencies = [ "num-traits", "solana-program", "spl-token", - "spl-token-2022", + "spl-token-2022 1.0.0", "thiserror", ] @@ -2179,6 +2181,20 @@ dependencies = [ "spl-type-length-value", ] +[[package]] +name = "spl-tlv-account-resolution" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615d381f48ddd2bb3c57c7f7fb207591a2a05054639b18a62e785117dd7a8683" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", +] + [[package]] name = "spl-token" version = "4.0.0" @@ -2204,18 +2220,55 @@ dependencies = [ "bytemuck", "num-derive 0.4.2", "num-traits", - "num_enum 0.7.0", + "num_enum 0.7.4", + "solana-program", + "solana-zk-token-sdk", + "spl-memo", + "spl-pod", + "spl-token", + "spl-token-metadata-interface", + "spl-transfer-hook-interface 0.3.0", + "spl-type-length-value", + "thiserror", +] + +[[package]] +name = "spl-token-2022" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d697fac19fd74ff472dfcc13f0b442dd71403178ce1de7b5d16f83a33561c059" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.4", "solana-program", + "solana-security-txt", "solana-zk-token-sdk", "spl-memo", "spl-pod", "spl-token", + "spl-token-group-interface", "spl-token-metadata-interface", - "spl-transfer-hook-interface", + "spl-transfer-hook-interface 0.4.1", "spl-type-length-value", "thiserror", ] +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b889509d49fa74a4a033ca5dae6c2307e9e918122d97e58562f5c4ffa795c75d" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + [[package]] name = "spl-token-metadata-interface" version = "0.2.0" @@ -2242,7 +2295,23 @@ dependencies = [ "spl-discriminator", "spl-pod", "spl-program-error", - "spl-tlv-account-resolution", + "spl-tlv-account-resolution 0.4.0", + "spl-type-length-value", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aabdb7c471566f6ddcee724beb8618449ea24b399e58d464d6b5bc7db550259" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution 0.5.1", "spl-type-length-value", ] diff --git a/programs/launchpad/Cargo.toml b/programs/launchpad/Cargo.toml index 68d4525f..e8cf3672 100644 --- a/programs/launchpad/Cargo.toml +++ b/programs/launchpad/Cargo.toml @@ -21,6 +21,7 @@ custom-heap = [] [dependencies] anchor-lang = "0.29.0" anchor-spl = "0.29.0" +spl-associated-token-account = "2.3.0" futarchy = { path = "../futarchy", features = ["cpi"] } price_based_performance_package = { path = "../price_based_performance_package", features = ["cpi"] } spl-memo = "=4.0.0" diff --git a/programs/launchpad/src/instructions/mod.rs b/programs/launchpad/src/instructions/mod.rs index 3cc8c52b..23481e70 100644 --- a/programs/launchpad/src/instructions/mod.rs +++ b/programs/launchpad/src/instructions/mod.rs @@ -5,6 +5,7 @@ pub mod fund; pub mod initialize_launch; pub mod refund; pub mod start_launch; +pub mod recover_nested_tokens; pub use claim::*; pub use close_launch::*; @@ -13,3 +14,4 @@ pub use fund::*; pub use initialize_launch::*; pub use refund::*; pub use start_launch::*; +pub use recover_nested_tokens::*; diff --git a/programs/launchpad/src/instructions/recover_nested_tokens.rs b/programs/launchpad/src/instructions/recover_nested_tokens.rs new file mode 100644 index 00000000..6d78419f --- /dev/null +++ b/programs/launchpad/src/instructions/recover_nested_tokens.rs @@ -0,0 +1,63 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token, TokenAccount}; + +use crate::{state::{Launch, associated_token_program::associated_token_program::{RecoverNested, recover_nested}}}; + +#[event_cpi] +#[derive(Accounts)] +pub struct RecoverNestedTokens<'info> { + #[account( + mut, + has_one = launch_quote_vault, + has_one = launch_signer, + )] + pub launch: Account<'info, Launch>, + + #[account(mut)] + pub launch_quote_vault: Account<'info, TokenAccount>, + #[account( + mut, + associated_token::authority = launch_quote_vault, + associated_token::mint = launch_quote_vault.mint + )] + pub nested_launch_quote_vault: Account<'info, TokenAccount>, + #[account( + address = launch_quote_vault.mint + )] + pub mint: Account<'info, Mint>, + + /// CHECK: just a signer + pub launch_signer: UncheckedAccount<'info>, + + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, +} + +impl RecoverNestedTokens<'_> { + pub fn handle(ctx: Context) -> Result<()> { + let seeds = &[ + b"launch_signer", + ctx.accounts.launch.to_account_info().key.as_ref(), + &[ctx.accounts.launch.launch_signer_pda_bump], + ]; + let signer = &[&seeds[..]]; + + let accounts = RecoverNested { + nested_associated_account_address: ctx.accounts.nested_launch_quote_vault.to_account_info(), + nested_associated_mint_address: ctx.accounts.mint.to_account_info(), + destination_associated_account_address: ctx.accounts.launch_quote_vault.to_account_info(), + owner_associated_account_address: ctx.accounts.launch_quote_vault.to_account_info(), + owner_token_mint_address: ctx.accounts.mint.to_account_info(), + wallet_address: ctx.accounts.launch_signer.to_account_info(), + token_program_id: ctx.accounts.token_program.to_account_info(), + }; + + recover_nested( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + accounts, + signer + ) + ) + } +} diff --git a/programs/launchpad/src/lib.rs b/programs/launchpad/src/lib.rs index 70a11abd..e6b32a32 100644 --- a/programs/launchpad/src/lib.rs +++ b/programs/launchpad/src/lib.rs @@ -90,4 +90,8 @@ pub mod launchpad { pub fn close_launch(ctx: Context) -> Result<()> { CloseLaunch::handle(ctx) } + + pub fn recover_nested_tokens(ctx: Context) -> Result<()> { + RecoverNestedTokens::handle(ctx) + } } diff --git a/programs/launchpad/src/state/associated_token_program.rs b/programs/launchpad/src/state/associated_token_program.rs new file mode 100644 index 00000000..7ee7f021 --- /dev/null +++ b/programs/launchpad/src/state/associated_token_program.rs @@ -0,0 +1,57 @@ +use anchor_lang::prelude::*; +use spl_associated_token_account::instruction::AssociatedTokenAccountInstruction; + +pub mod associated_token_program { + use solana_program::instruction::Instruction; + + use super::*; + + declare_id!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); + + pub fn recover_nested<'info>( + ctx: CpiContext<'_, '_, '_, 'info, RecoverNested<'info>>, + ) -> Result<()> { + + let instruction_data = AssociatedTokenAccountInstruction::RecoverNested; + + let ix = Instruction { + program_id: id(), + accounts: vec![ + AccountMeta::new(ctx.accounts.nested_associated_account_address.key(), false), + AccountMeta::new_readonly(ctx.accounts.nested_associated_mint_address.key(), false), + AccountMeta::new(ctx.accounts.destination_associated_account_address.key(), false), + AccountMeta::new_readonly(ctx.accounts.owner_associated_account_address.key(), false), + AccountMeta::new_readonly(ctx.accounts.owner_token_mint_address.key(), false), + AccountMeta::new(ctx.accounts.wallet_address.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_program_id.key(), false), + ], + data: instruction_data.try_to_vec().unwrap() + }; + + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.nested_associated_account_address, + ctx.accounts.nested_associated_mint_address, + ctx.accounts.destination_associated_account_address, + ctx.accounts.owner_associated_account_address, + ctx.accounts.owner_token_mint_address, + ctx.accounts.wallet_address, + ctx.accounts.token_program_id + ], + ctx.signer_seeds, + ) + .map_err(Into::into) + } + + #[derive(Accounts)] + pub struct RecoverNested<'info> { + pub nested_associated_account_address: AccountInfo<'info>, + pub nested_associated_mint_address: AccountInfo<'info>, + pub destination_associated_account_address: AccountInfo<'info>, + pub owner_associated_account_address: AccountInfo<'info>, + pub owner_token_mint_address: AccountInfo<'info>, + pub wallet_address: AccountInfo<'info>, + pub token_program_id: AccountInfo<'info>, + } +} \ No newline at end of file diff --git a/programs/launchpad/src/state/mod.rs b/programs/launchpad/src/state/mod.rs index d1d1addd..f586d846 100644 --- a/programs/launchpad/src/state/mod.rs +++ b/programs/launchpad/src/state/mod.rs @@ -1,5 +1,7 @@ +pub mod associated_token_program; pub mod funding_record; pub mod launch; +pub use associated_token_program::*; pub use funding_record::*; pub use launch::*; From 44e1bfe36f0a7af8a8eccdc4f36ebd154a89d20a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dean=20=E5=88=A9=E8=BF=AA=E6=81=A9?= Date: Tue, 14 Oct 2025 12:17:03 +0800 Subject: [PATCH 2/2] Fixes --- Cargo.lock | 1 - programs/launchpad/Cargo.toml | 1 - .../src/instructions/recover_nested_tokens.rs | 11 +- .../src/state/associated_token_program.rs | 107 +++++++++--------- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7655f0d..b92b8677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1152,7 +1152,6 @@ dependencies = [ "price_based_performance_package", "solana-program", "solana-security-txt", - "spl-associated-token-account", "spl-memo", "spl-token", "squads-multisig-program", diff --git a/programs/launchpad/Cargo.toml b/programs/launchpad/Cargo.toml index e8cf3672..68d4525f 100644 --- a/programs/launchpad/Cargo.toml +++ b/programs/launchpad/Cargo.toml @@ -21,7 +21,6 @@ custom-heap = [] [dependencies] anchor-lang = "0.29.0" anchor-spl = "0.29.0" -spl-associated-token-account = "2.3.0" futarchy = { path = "../futarchy", features = ["cpi"] } price_based_performance_package = { path = "../price_based_performance_package", features = ["cpi"] } spl-memo = "=4.0.0" diff --git a/programs/launchpad/src/instructions/recover_nested_tokens.rs b/programs/launchpad/src/instructions/recover_nested_tokens.rs index 6d78419f..e866aab3 100644 --- a/programs/launchpad/src/instructions/recover_nested_tokens.rs +++ b/programs/launchpad/src/instructions/recover_nested_tokens.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token, TokenAccount}; +use anchor_spl::{associated_token::AssociatedToken, token::{Mint, Token, TokenAccount}}; -use crate::{state::{Launch, associated_token_program::associated_token_program::{RecoverNested, recover_nested}}}; +use crate::{state::{Launch, associated_token_program::{RecoverNested, recover_nested}}}; #[event_cpi] #[derive(Accounts)] @@ -30,7 +30,7 @@ pub struct RecoverNestedTokens<'info> { pub launch_signer: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, - pub system_program: Program<'info, System>, + pub associated_token_program: Program<'info, AssociatedToken>, } impl RecoverNestedTokens<'_> { @@ -49,12 +49,13 @@ impl RecoverNestedTokens<'_> { owner_associated_account_address: ctx.accounts.launch_quote_vault.to_account_info(), owner_token_mint_address: ctx.accounts.mint.to_account_info(), wallet_address: ctx.accounts.launch_signer.to_account_info(), - token_program_id: ctx.accounts.token_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + associated_token_program: ctx.accounts.associated_token_program.to_account_info(), }; recover_nested( CpiContext::new_with_signer( - ctx.accounts.token_program.to_account_info(), + ctx.accounts.associated_token_program.to_account_info(), accounts, signer ) diff --git a/programs/launchpad/src/state/associated_token_program.rs b/programs/launchpad/src/state/associated_token_program.rs index 7ee7f021..1f59acfc 100644 --- a/programs/launchpad/src/state/associated_token_program.rs +++ b/programs/launchpad/src/state/associated_token_program.rs @@ -1,57 +1,56 @@ use anchor_lang::prelude::*; -use spl_associated_token_account::instruction::AssociatedTokenAccountInstruction; -pub mod associated_token_program { - use solana_program::instruction::Instruction; - - use super::*; - - declare_id!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); - - pub fn recover_nested<'info>( - ctx: CpiContext<'_, '_, '_, 'info, RecoverNested<'info>>, - ) -> Result<()> { - - let instruction_data = AssociatedTokenAccountInstruction::RecoverNested; - - let ix = Instruction { - program_id: id(), - accounts: vec![ - AccountMeta::new(ctx.accounts.nested_associated_account_address.key(), false), - AccountMeta::new_readonly(ctx.accounts.nested_associated_mint_address.key(), false), - AccountMeta::new(ctx.accounts.destination_associated_account_address.key(), false), - AccountMeta::new_readonly(ctx.accounts.owner_associated_account_address.key(), false), - AccountMeta::new_readonly(ctx.accounts.owner_token_mint_address.key(), false), - AccountMeta::new(ctx.accounts.wallet_address.key(), true), - AccountMeta::new_readonly(ctx.accounts.token_program_id.key(), false), - ], - data: instruction_data.try_to_vec().unwrap() - }; - - solana_program::program::invoke_signed( - &ix, - &[ - ctx.accounts.nested_associated_account_address, - ctx.accounts.nested_associated_mint_address, - ctx.accounts.destination_associated_account_address, - ctx.accounts.owner_associated_account_address, - ctx.accounts.owner_token_mint_address, - ctx.accounts.wallet_address, - ctx.accounts.token_program_id - ], - ctx.signer_seeds, - ) - .map_err(Into::into) - } - - #[derive(Accounts)] - pub struct RecoverNested<'info> { - pub nested_associated_account_address: AccountInfo<'info>, - pub nested_associated_mint_address: AccountInfo<'info>, - pub destination_associated_account_address: AccountInfo<'info>, - pub owner_associated_account_address: AccountInfo<'info>, - pub owner_token_mint_address: AccountInfo<'info>, - pub wallet_address: AccountInfo<'info>, - pub token_program_id: AccountInfo<'info>, - } +use anchor_spl::associated_token::AssociatedToken; +use solana_program::instruction::Instruction; + +use super::*; + +pub fn recover_nested<'info>( + ctx: CpiContext<'_, '_, '_, 'info, RecoverNested<'info>>, +) -> Result<()> { + + // RecoverNested instruction + let instruction_data = vec![2]; + + let ix = Instruction { + program_id: ctx.accounts.associated_token_program.key(), + accounts: vec![ + AccountMeta::new(ctx.accounts.nested_associated_account_address.key(), false), + AccountMeta::new_readonly(ctx.accounts.nested_associated_mint_address.key(), false), + AccountMeta::new(ctx.accounts.destination_associated_account_address.key(), false), + AccountMeta::new_readonly(ctx.accounts.owner_associated_account_address.key(), false), + AccountMeta::new_readonly(ctx.accounts.owner_token_mint_address.key(), false), + AccountMeta::new(ctx.accounts.wallet_address.key(), true), + AccountMeta::new_readonly(ctx.accounts.token_program.key(), false), + ], + data: instruction_data.try_to_vec().unwrap() + }; + + solana_program::program::invoke_signed( + &ix, + &[ + ctx.accounts.nested_associated_account_address, + ctx.accounts.nested_associated_mint_address, + ctx.accounts.destination_associated_account_address, + ctx.accounts.owner_associated_account_address, + ctx.accounts.owner_token_mint_address, + ctx.accounts.wallet_address, + ctx.accounts.token_program + ], + ctx.signer_seeds, + ) + .map_err(Into::into) +} + +#[derive(Accounts)] +pub struct RecoverNested<'info> { + pub nested_associated_account_address: AccountInfo<'info>, + pub nested_associated_mint_address: AccountInfo<'info>, + pub destination_associated_account_address: AccountInfo<'info>, + pub owner_associated_account_address: AccountInfo<'info>, + pub owner_token_mint_address: AccountInfo<'info>, + pub wallet_address: AccountInfo<'info>, + pub token_program: AccountInfo<'info>, + #[account(address = AssociatedToken::id())] + pub associated_token_program: AccountInfo<'info>, } \ No newline at end of file