Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 182c053

Browse files
authored
associated-token-account: Add recover nested account ix (#2889)
* associated-token-account: Add transfer / close nested accounts * Swap wallet and wrong_wallet in test * Use new error * Force destination to wallet and ATA * Fix merge conflicts * Add more vanilla spl-token tests * Improve test, fix instruction comments * Address feedback * Rename CloseNested -> RecoverNested, add comment * Fix typo in comment
1 parent b7a3fc6 commit 182c053

File tree

8 files changed

+899
-51
lines changed

8 files changed

+899
-51
lines changed

associated-token-account/program/src/instruction.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ pub enum AssociatedTokenAccountInstruction {
3434
/// 4. `[]` System program
3535
/// 5. `[]` SPL Token program
3636
CreateIdempotent,
37+
/// Transfers from and closes a nested associated token account: an
38+
/// associated token account owned by an associated token account.
39+
///
40+
/// The tokens are moved from the nested associated token account to the
41+
/// wallet's associated token account, and the nested account lamports are
42+
/// moved to the wallet.
43+
///
44+
/// Note: Nested token accounts are an anti-pattern, and almost always
45+
/// created unintentionally, so this instruction should only be used to
46+
/// recover from errors.
47+
///
48+
/// 0. `[writeable]` Nested associated token account, must be owned by `3`
49+
/// 1. `[]` Token mint for the nested associated token account
50+
/// 2. `[writeable]` Wallet's associated token account
51+
/// 3. `[]` Owner associated token account address, must be owned by `5`
52+
/// 4. `[]` Token mint for the owner associated token account
53+
/// 5. `[writeable, signer]` Wallet address for the owner associated token account
54+
/// 6. `[]` SPL Token program
55+
RecoverNested,
3756
}
3857

3958
fn build_associated_token_account_instruction(
@@ -99,3 +118,43 @@ pub fn create_associated_token_account_idempotent(
99118
AssociatedTokenAccountInstruction::CreateIdempotent,
100119
)
101120
}
121+
122+
/// Creates a `RecoverNested` instruction
123+
pub fn recover_nested(
124+
wallet_address: &Pubkey,
125+
owner_token_mint_address: &Pubkey,
126+
nested_token_mint_address: &Pubkey,
127+
token_program_id: &Pubkey,
128+
) -> Instruction {
129+
let owner_associated_account_address = get_associated_token_address_with_program_id(
130+
wallet_address,
131+
owner_token_mint_address,
132+
token_program_id,
133+
);
134+
let destination_associated_account_address = get_associated_token_address_with_program_id(
135+
wallet_address,
136+
nested_token_mint_address,
137+
token_program_id,
138+
);
139+
let nested_associated_account_address = get_associated_token_address_with_program_id(
140+
&owner_associated_account_address, // ATA is wrongly used as a wallet_address
141+
nested_token_mint_address,
142+
token_program_id,
143+
);
144+
145+
let instruction_data = AssociatedTokenAccountInstruction::RecoverNested;
146+
147+
Instruction {
148+
program_id: id(),
149+
accounts: vec![
150+
AccountMeta::new(nested_associated_account_address, false),
151+
AccountMeta::new_readonly(*nested_token_mint_address, false),
152+
AccountMeta::new(destination_associated_account_address, false),
153+
AccountMeta::new_readonly(owner_associated_account_address, false),
154+
AccountMeta::new_readonly(*owner_token_mint_address, false),
155+
AccountMeta::new(*wallet_address, true),
156+
AccountMeta::new_readonly(*token_program_id, false),
157+
],
158+
data: instruction_data.try_to_vec().unwrap(),
159+
}
160+
}

associated-token-account/program/src/processor.rs

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use {
1212
account_info::{next_account_info, AccountInfo},
1313
entrypoint::ProgramResult,
1414
msg,
15-
program::invoke,
15+
program::{invoke, invoke_signed},
1616
program_error::ProgramError,
1717
pubkey::Pubkey,
1818
rent::Rent,
@@ -21,7 +21,7 @@ use {
2121
},
2222
spl_token_2022::{
2323
extension::{ExtensionType, StateWithExtensions},
24-
state::Account,
24+
state::{Account, Mint},
2525
},
2626
};
2727

@@ -56,6 +56,9 @@ pub fn process_instruction(
5656
AssociatedTokenAccountInstruction::CreateIdempotent => {
5757
process_create_associated_token_account(program_id, accounts, CreateMode::Idempotent)
5858
}
59+
AssociatedTokenAccountInstruction::RecoverNested => {
60+
process_recover_nested(program_id, accounts)
61+
}
5962
}
6063
}
6164

@@ -157,3 +160,150 @@ fn process_create_associated_token_account(
157160
],
158161
)
159162
}
163+
164+
/// Processes `RecoverNested` instruction
165+
pub fn process_recover_nested(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
166+
let account_info_iter = &mut accounts.iter();
167+
168+
let nested_associated_token_account_info = next_account_info(account_info_iter)?;
169+
let nested_token_mint_info = next_account_info(account_info_iter)?;
170+
let destination_associated_token_account_info = next_account_info(account_info_iter)?;
171+
let owner_associated_token_account_info = next_account_info(account_info_iter)?;
172+
let owner_token_mint_info = next_account_info(account_info_iter)?;
173+
let wallet_account_info = next_account_info(account_info_iter)?;
174+
let spl_token_program_info = next_account_info(account_info_iter)?;
175+
let spl_token_program_id = spl_token_program_info.key;
176+
177+
// Check owner address derivation
178+
let (owner_associated_token_address, bump_seed) =
179+
get_associated_token_address_and_bump_seed_internal(
180+
wallet_account_info.key,
181+
owner_token_mint_info.key,
182+
program_id,
183+
spl_token_program_id,
184+
);
185+
if owner_associated_token_address != *owner_associated_token_account_info.key {
186+
msg!("Error: Owner associated address does not match seed derivation");
187+
return Err(ProgramError::InvalidSeeds);
188+
}
189+
190+
// Check nested address derivation
191+
let (nested_associated_token_address, _) = get_associated_token_address_and_bump_seed_internal(
192+
owner_associated_token_account_info.key,
193+
nested_token_mint_info.key,
194+
program_id,
195+
spl_token_program_id,
196+
);
197+
if nested_associated_token_address != *nested_associated_token_account_info.key {
198+
msg!("Error: Nested associated address does not match seed derivation");
199+
return Err(ProgramError::InvalidSeeds);
200+
}
201+
202+
// Check destination address derivation
203+
let (destination_associated_token_address, _) =
204+
get_associated_token_address_and_bump_seed_internal(
205+
wallet_account_info.key,
206+
nested_token_mint_info.key,
207+
program_id,
208+
spl_token_program_id,
209+
);
210+
if destination_associated_token_address != *destination_associated_token_account_info.key {
211+
msg!("Error: Destination associated address does not match seed derivation");
212+
return Err(ProgramError::InvalidSeeds);
213+
}
214+
215+
if !wallet_account_info.is_signer {
216+
msg!("Wallet of the owner associated token account must sign");
217+
return Err(ProgramError::MissingRequiredSignature);
218+
}
219+
220+
if owner_token_mint_info.owner != spl_token_program_id {
221+
msg!("Owner mint not owned by provided token program");
222+
return Err(ProgramError::IllegalOwner);
223+
}
224+
225+
// Account data is dropped at the end of this, so the CPI can succeed
226+
// without a double-borrow
227+
let (amount, decimals) = {
228+
// Check owner associated token account data
229+
if owner_associated_token_account_info.owner != spl_token_program_id {
230+
msg!("Owner associated token account not owned by provided token program, recreate the owner associated token account first");
231+
return Err(ProgramError::IllegalOwner);
232+
}
233+
let owner_account_data = owner_associated_token_account_info.data.borrow();
234+
let owner_account = StateWithExtensions::<Account>::unpack(&owner_account_data)?;
235+
if owner_account.base.owner != *wallet_account_info.key {
236+
msg!("Owner associated token account not owned by provided wallet");
237+
return Err(AssociatedTokenAccountError::InvalidOwner.into());
238+
}
239+
240+
// Check nested associated token account data
241+
if nested_associated_token_account_info.owner != spl_token_program_id {
242+
msg!("Nested associated token account not owned by provided token program");
243+
return Err(ProgramError::IllegalOwner);
244+
}
245+
let nested_account_data = nested_associated_token_account_info.data.borrow();
246+
let nested_account = StateWithExtensions::<Account>::unpack(&nested_account_data)?;
247+
if nested_account.base.owner != *owner_associated_token_account_info.key {
248+
msg!("Nested associated token account not owned by provided associated token account");
249+
return Err(AssociatedTokenAccountError::InvalidOwner.into());
250+
}
251+
let amount = nested_account.base.amount;
252+
253+
// Check nested token mint data
254+
if nested_token_mint_info.owner != spl_token_program_id {
255+
msg!("Nested mint account not owned by provided token program");
256+
return Err(ProgramError::IllegalOwner);
257+
}
258+
let nested_mint_data = nested_token_mint_info.data.borrow();
259+
let nested_mint = StateWithExtensions::<Mint>::unpack(&nested_mint_data)?;
260+
let decimals = nested_mint.base.decimals;
261+
(amount, decimals)
262+
};
263+
264+
// Transfer everything out
265+
let owner_associated_token_account_signer_seeds: &[&[_]] = &[
266+
&wallet_account_info.key.to_bytes(),
267+
&spl_token_program_id.to_bytes(),
268+
&owner_token_mint_info.key.to_bytes(),
269+
&[bump_seed],
270+
];
271+
invoke_signed(
272+
&spl_token_2022::instruction::transfer_checked(
273+
spl_token_program_id,
274+
nested_associated_token_account_info.key,
275+
nested_token_mint_info.key,
276+
destination_associated_token_account_info.key,
277+
owner_associated_token_account_info.key,
278+
&[],
279+
amount,
280+
decimals,
281+
)?,
282+
&[
283+
nested_associated_token_account_info.clone(),
284+
nested_token_mint_info.clone(),
285+
destination_associated_token_account_info.clone(),
286+
owner_associated_token_account_info.clone(),
287+
spl_token_program_info.clone(),
288+
],
289+
&[owner_associated_token_account_signer_seeds],
290+
)?;
291+
292+
// Close the nested account so it's never used again
293+
invoke_signed(
294+
&spl_token_2022::instruction::close_account(
295+
spl_token_program_id,
296+
nested_associated_token_account_info.key,
297+
wallet_account_info.key,
298+
owner_associated_token_account_info.key,
299+
&[],
300+
)?,
301+
&[
302+
nested_associated_token_account_info.clone(),
303+
wallet_account_info.clone(),
304+
owner_associated_token_account_info.clone(),
305+
spl_token_program_info.clone(),
306+
],
307+
&[owner_associated_token_account_signer_seeds],
308+
)
309+
}

associated-token-account/program/tests/create_idempotent.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
mod program_test;
44

55
use {
6-
program_test::program_test,
6+
program_test::program_test_2022,
77
solana_program::{instruction::*, pubkey::Pubkey},
88
solana_program_test::*,
99
solana_sdk::{
@@ -40,7 +40,7 @@ async fn success_account_exists() {
4040
);
4141

4242
let (mut banks_client, payer, recent_blockhash) =
43-
program_test(token_mint_address, true).start().await;
43+
program_test_2022(token_mint_address, true).start().await;
4444
let rent = banks_client.get_rent().await.unwrap();
4545
let expected_token_account_len =
4646
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
@@ -150,7 +150,7 @@ async fn fail_account_exists_with_wrong_owner() {
150150
close_authority: COption::None,
151151
};
152152
Account::pack(token_account, &mut associated_token_account.data).unwrap();
153-
let mut pt = program_test(token_mint_address, true);
153+
let mut pt = program_test_2022(token_mint_address, true);
154154
pt.add_account(associated_token_address, associated_token_account);
155155
let (mut banks_client, payer, recent_blockhash) = pt.start().await;
156156

@@ -185,7 +185,7 @@ async fn fail_account_exists_with_wrong_owner() {
185185
async fn fail_non_ata() {
186186
let token_mint_address = Pubkey::new_unique();
187187
let (mut banks_client, payer, recent_blockhash) =
188-
program_test(token_mint_address, true).start().await;
188+
program_test_2022(token_mint_address, true).start().await;
189189

190190
let rent = banks_client.get_rent().await.unwrap();
191191
let token_account_len =

associated-token-account/program/tests/extended_mint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
mod program_test;
55

66
use {
7-
program_test::program_test,
7+
program_test::program_test_2022,
88
solana_program::{instruction::*, pubkey::Pubkey, system_instruction},
99
solana_program_test::*,
1010
solana_sdk::{
@@ -29,7 +29,7 @@ async fn test_associated_token_account_with_transfer_fees() {
2929
let wallet_receiver = Keypair::new();
3030
let wallet_address_receiver = wallet_receiver.pubkey();
3131
let (mut banks_client, payer, recent_blockhash) =
32-
program_test(Pubkey::new_unique(), true).start().await;
32+
program_test_2022(Pubkey::new_unique(), true).start().await;
3333
let rent = banks_client.get_rent().await.unwrap();
3434

3535
// create extended mint

associated-token-account/program/tests/process_create_associated_token_account.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
mod program_test;
55

66
use {
7-
program_test::program_test,
7+
program_test::program_test_2022,
88
solana_program::{instruction::*, pubkey::Pubkey, system_instruction, sysvar},
99
solana_program_test::*,
1010
solana_sdk::{
@@ -28,7 +28,7 @@ async fn test_associated_token_address() {
2828
);
2929

3030
let (mut banks_client, payer, recent_blockhash) =
31-
program_test(token_mint_address, true).start().await;
31+
program_test_2022(token_mint_address, true).start().await;
3232
let rent = banks_client.get_rent().await.unwrap();
3333

3434
let expected_token_account_len =
@@ -78,7 +78,7 @@ async fn test_create_with_fewer_lamports() {
7878
);
7979

8080
let (mut banks_client, payer, recent_blockhash) =
81-
program_test(token_mint_address, true).start().await;
81+
program_test_2022(token_mint_address, true).start().await;
8282
let rent = banks_client.get_rent().await.unwrap();
8383
let expected_token_account_len =
8484
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);
@@ -138,7 +138,7 @@ async fn test_create_with_excess_lamports() {
138138
);
139139

140140
let (mut banks_client, payer, recent_blockhash) =
141-
program_test(token_mint_address, true).start().await;
141+
program_test_2022(token_mint_address, true).start().await;
142142
let rent = banks_client.get_rent().await.unwrap();
143143

144144
let expected_token_account_len =
@@ -198,7 +198,7 @@ async fn test_create_account_mismatch() {
198198
);
199199

200200
let (mut banks_client, payer, recent_blockhash) =
201-
program_test(token_mint_address, true).start().await;
201+
program_test_2022(token_mint_address, true).start().await;
202202

203203
let mut instruction = create_associated_token_account(
204204
&payer.pubkey(),
@@ -269,7 +269,7 @@ async fn test_create_associated_token_account_using_legacy_implicit_instruction(
269269
);
270270

271271
let (mut banks_client, payer, recent_blockhash) =
272-
program_test(token_mint_address, true).start().await;
272+
program_test_2022(token_mint_address, true).start().await;
273273
let rent = banks_client.get_rent().await.unwrap();
274274
let expected_token_account_len =
275275
ExtensionType::get_account_len::<Account>(&[ExtensionType::ImmutableOwner]);

0 commit comments

Comments
 (0)