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

Commit 290d8d8

Browse files
authored
associated-token-account: Add "CreateIdempotent" instruction (#2883)
* associated-token-account: Add "CreateIfNonExistent" instruction * Address feedback * Move ownership check
1 parent 428d93b commit 290d8d8

File tree

8 files changed

+304
-10
lines changed

8 files changed

+304
-10
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

associated-token-account/program/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ no-entrypoint = []
1212
test-bpf = []
1313

1414
[dependencies]
15+
assert_matches = "1.5.0"
1516
borsh = "0.9.1"
17+
num-derive = "0.3"
18+
num-traits = "0.2"
1619
solana-program = "1.9.5"
1720
spl-token = { version = "0.1", path = "../../token/program-2022", package = "spl-token-2022", features = ["no-entrypoint"] }
21+
thiserror = "1.0"
1822

1923
[dev-dependencies]
2024
solana-program-test = "1.9.5"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//! Error types
2+
3+
use {
4+
num_derive::FromPrimitive,
5+
solana_program::{decode_error::DecodeError, program_error::ProgramError},
6+
thiserror::Error,
7+
};
8+
9+
/// Errors that may be returned by the program.
10+
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
11+
pub enum AssociatedTokenAccountError {
12+
// 0
13+
/// Associated token account owner does not match address derivation
14+
#[error("Associated token account owner does not match address derivation")]
15+
InvalidOwner,
16+
}
17+
impl From<AssociatedTokenAccountError> for ProgramError {
18+
fn from(e: AssociatedTokenAccountError) -> Self {
19+
ProgramError::Custom(e as u32)
20+
}
21+
}
22+
impl<T> DecodeError<T> for AssociatedTokenAccountError {
23+
fn type_of() -> &'static str {
24+
"AssociatedTokenAccountError"
25+
}
26+
}

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

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Program instructions
22
33
use {
4+
assert_matches::assert_matches,
45
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
56
solana_program::{
67
instruction::{AccountMeta, Instruction},
@@ -14,6 +15,7 @@ use crate::{get_associated_token_address, id};
1415
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
1516
pub enum AssociatedTokenAccountInstruction {
1617
/// Creates an associated token account for the given wallet address and token mint
18+
/// Returns an error if the account exists.
1719
///
1820
/// 0. `[writeable,signer]` Funding account (must be a system account)
1921
/// 1. `[writeable]` Associated token account address to be created
@@ -22,20 +24,34 @@ pub enum AssociatedTokenAccountInstruction {
2224
/// 4. `[]` System program
2325
/// 5. `[]` SPL Token program
2426
Create,
27+
/// Creates an associated token account for the given wallet address and token mint,
28+
/// if it doesn't already exist. Returns an error if the account exists,
29+
/// but with a different owner.
30+
///
31+
/// 0. `[writeable,signer]` Funding account (must be a system account)
32+
/// 1. `[writeable]` Associated token account address to be created
33+
/// 2. `[]` Wallet address for the new associated token account
34+
/// 3. `[]` The token mint for the new associated token account
35+
/// 4. `[]` System program
36+
/// 5. `[]` SPL Token program
37+
CreateIdempotent,
2538
}
2639

27-
/// Creates CreateAssociatedTokenAccount instruction
28-
pub fn create_associated_token_account(
40+
fn build_associated_token_account_instruction(
2941
funding_address: &Pubkey,
3042
wallet_address: &Pubkey,
3143
token_mint_address: &Pubkey,
3244
token_program_id: &Pubkey,
45+
instruction: AssociatedTokenAccountInstruction,
3346
) -> Instruction {
3447
let associated_account_address =
3548
get_associated_token_address(wallet_address, token_mint_address);
36-
37-
let instruction_data = AssociatedTokenAccountInstruction::Create {};
38-
49+
// safety check, assert if not a creation instruction
50+
assert_matches!(
51+
instruction,
52+
AssociatedTokenAccountInstruction::Create
53+
| AssociatedTokenAccountInstruction::CreateIdempotent
54+
);
3955
Instruction {
4056
program_id: id(),
4157
accounts: vec![
@@ -46,6 +62,38 @@ pub fn create_associated_token_account(
4662
AccountMeta::new_readonly(solana_program::system_program::id(), false),
4763
AccountMeta::new_readonly(*token_program_id, false),
4864
],
49-
data: instruction_data.try_to_vec().unwrap(),
65+
data: instruction.try_to_vec().unwrap(),
5066
}
5167
}
68+
69+
/// Creates Create instruction
70+
pub fn create_associated_token_account(
71+
funding_address: &Pubkey,
72+
wallet_address: &Pubkey,
73+
token_mint_address: &Pubkey,
74+
token_program_id: &Pubkey,
75+
) -> Instruction {
76+
build_associated_token_account_instruction(
77+
funding_address,
78+
wallet_address,
79+
token_mint_address,
80+
token_program_id,
81+
AssociatedTokenAccountInstruction::Create,
82+
)
83+
}
84+
85+
/// Creates CreateIdempotent instruction
86+
pub fn create_associated_token_account_idempotent(
87+
funding_address: &Pubkey,
88+
wallet_address: &Pubkey,
89+
token_mint_address: &Pubkey,
90+
token_program_id: &Pubkey,
91+
) -> Instruction {
92+
build_associated_token_account_instruction(
93+
funding_address,
94+
wallet_address,
95+
token_mint_address,
96+
token_program_id,
97+
AssociatedTokenAccountInstruction::CreateIdempotent,
98+
)
99+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#![forbid(unsafe_code)]
44

55
mod entrypoint;
6+
pub mod error;
67
pub mod instruction;
78
pub mod processor;
89
pub mod tools;

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use {
44
crate::{
5+
error::AssociatedTokenAccountError,
56
instruction::AssociatedTokenAccountInstruction,
67
tools::account::{create_pda_account, get_account_len},
78
*,
@@ -15,10 +16,21 @@ use {
1516
program_error::ProgramError,
1617
pubkey::Pubkey,
1718
rent::Rent,
19+
system_program,
1820
sysvar::Sysvar,
1921
},
22+
spl_token::{extension::StateWithExtensions, state::Account},
2023
};
2124

25+
/// Specify when to create the associated token account
26+
#[derive(PartialEq)]
27+
enum CreateMode {
28+
/// Always try to create the ATA
29+
Always,
30+
/// Only try to create the ATA if non-existent
31+
Idempotent,
32+
}
33+
2234
/// Instruction processor
2335
pub fn process_instruction(
2436
program_id: &Pubkey,
@@ -35,16 +47,20 @@ pub fn process_instruction(
3547
msg!("{:?}", instruction);
3648

3749
match instruction {
38-
AssociatedTokenAccountInstruction::Create {} => {
39-
process_create_associated_token_account(program_id, accounts)
50+
AssociatedTokenAccountInstruction::Create => {
51+
process_create_associated_token_account(program_id, accounts, CreateMode::Always)
52+
}
53+
AssociatedTokenAccountInstruction::CreateIdempotent => {
54+
process_create_associated_token_account(program_id, accounts, CreateMode::Idempotent)
4055
}
4156
}
4257
}
4358

4459
/// Processes CreateAssociatedTokenAccount instruction
45-
pub fn process_create_associated_token_account(
60+
fn process_create_associated_token_account(
4661
program_id: &Pubkey,
4762
accounts: &[AccountInfo],
63+
create_mode: CreateMode,
4864
) -> ProgramResult {
4965
let account_info_iter = &mut accounts.iter();
5066

@@ -56,6 +72,26 @@ pub fn process_create_associated_token_account(
5672
let spl_token_program_info = next_account_info(account_info_iter)?;
5773
let spl_token_program_id = spl_token_program_info.key;
5874

75+
if create_mode == CreateMode::Idempotent
76+
&& associated_token_account_info.owner == spl_token_program_id
77+
{
78+
let ata_data = associated_token_account_info.data.borrow();
79+
if let Ok(associated_token_account) = StateWithExtensions::<Account>::unpack(&ata_data) {
80+
if associated_token_account.base.owner != *wallet_account_info.key {
81+
let error = AssociatedTokenAccountError::InvalidOwner;
82+
msg!("{}", error);
83+
return Err(error.into());
84+
}
85+
if associated_token_account.base.mint != *spl_token_mint_info.key {
86+
return Err(ProgramError::InvalidAccountData);
87+
}
88+
return Ok(());
89+
}
90+
}
91+
if *associated_token_account_info.owner != system_program::id() {
92+
return Err(ProgramError::IllegalOwner);
93+
}
94+
5995
let rent = Rent::get()?;
6096

6197
let (associated_token_address, bump_seed) = get_associated_token_address_and_bump_seed_internal(

0 commit comments

Comments
 (0)