Skip to content

Commit 59f3325

Browse files
lang, spl, cli: Add associated_token keyword (otter-sec#790)
1 parent 7a21142 commit 59f3325

12 files changed

Lines changed: 276 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ incremented for features.
1111

1212
## [Unreleased]
1313

14-
## [0.16.1] - 2021-09-17
15-
1614
### Features
1715

1816
* lang: Add `--detach` flag to `anchor test` ([#770](https://github.com/project-serum/anchor/pull/770)).
17+
* lang: Add `associated_token` keyword for initializing associated token accounts within `#[derive(Accounts)]` ([#790](https://github.com/project-serum/anchor/pull/790)).
18+
19+
## [0.16.1] - 2021-09-17
1920

2021
### Fixes
2122

Cargo.lock

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

cli/src/lib.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,18 +1370,10 @@ fn test(
13701370
.context(cmd)
13711371
};
13721372

1373-
match test_result {
1374-
Ok(exit) => {
1375-
if detach {
1376-
println!("Local validator still running. Press Ctrl + C quit.");
1377-
std::io::stdin().lock().lines().next().unwrap().unwrap();
1378-
} else if !exit.status.success() && !detach {
1379-
std::process::exit(exit.status.code().unwrap());
1380-
}
1381-
}
1382-
Err(err) => {
1383-
println!("Failed to run test: {:#}", err)
1384-
}
1373+
// Keep validator running if needed.
1374+
if test_result.is_ok() && detach {
1375+
println!("Local validator still running. Press Ctrl + C quit.");
1376+
std::io::stdin().lock().lines().next().unwrap().unwrap();
13851377
}
13861378

13871379
// Check all errors and shut down.
@@ -1396,6 +1388,18 @@ fn test(
13961388
}
13971389
}
13981390

1391+
// Must exist *after* shutting down the validator and log streams.
1392+
match test_result {
1393+
Ok(exit) => {
1394+
if !exit.status.success() {
1395+
std::process::exit(exit.status.code().unwrap());
1396+
}
1397+
}
1398+
Err(err) => {
1399+
println!("Failed to run test: {:#}", err)
1400+
}
1401+
}
1402+
13991403
Ok(())
14001404
})
14011405
}

lang/syn/src/codegen/accounts/constraints.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,28 @@ pub fn generate_init(
411411
};
412412
}
413413
}
414+
InitKind::AssociatedToken { owner, mint } => {
415+
quote! {
416+
let #field: #ty_decl = {
417+
#payer
418+
419+
let cpi_program = associated_token_program.to_account_info();
420+
let cpi_accounts = anchor_spl::associated_token::Create {
421+
payer: payer.to_account_info(),
422+
associated_token: #field.to_account_info(),
423+
authority: #owner.to_account_info(),
424+
mint: #mint.to_account_info(),
425+
system_program: system_program.to_account_info(),
426+
token_program: token_program.to_account_info(),
427+
rent: rent.to_account_info(),
428+
};
429+
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
430+
anchor_spl::associated_token::create(cpi_ctx)?;
431+
let pa: #ty_decl = #from_account_info;
432+
pa
433+
};
434+
}
435+
}
414436
InitKind::Mint { owner, decimals } => {
415437
let create_account = generate_create_account(
416438
field,

lang/syn/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ impl Field {
243243
}
244244
}
245245
}
246+
Ty::CpiAccount(_) => {
247+
quote! {
248+
#container_ty::try_from_unchecked(
249+
&#field,
250+
)?
251+
}
252+
}
246253
_ => {
247254
let owner_addr = match &kind {
248255
None => quote! { program_id },
@@ -554,6 +561,8 @@ pub enum ConstraintToken {
554561
Address(Context<ConstraintAddress>),
555562
TokenMint(Context<ConstraintTokenMint>),
556563
TokenAuthority(Context<ConstraintTokenAuthority>),
564+
AssociatedTokenMint(Context<ConstraintTokenMint>),
565+
AssociatedTokenAuthority(Context<ConstraintTokenAuthority>),
557566
MintAuthority(Context<ConstraintMintAuthority>),
558567
MintDecimals(Context<ConstraintMintDecimals>),
559568
Bump(Context<ConstraintTokenBump>),
@@ -653,6 +662,7 @@ pub enum InitKind {
653662
// Owner for token and mint represents the authority. Not to be confused
654663
// with the owner of the AccountInfo.
655664
Token { owner: Expr, mint: Expr },
665+
AssociatedToken { owner: Expr, mint: Expr },
656666
Mint { owner: Expr, decimals: Expr },
657667
}
658668

lang/syn/src/parser/accounts/constraints.rs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,33 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
121121
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
122122
}
123123
}
124+
"associated_token" => {
125+
stream.parse::<Token![:]>()?;
126+
stream.parse::<Token![:]>()?;
127+
let kw = stream.call(Ident::parse_any)?.to_string();
128+
stream.parse::<Token![=]>()?;
129+
130+
let span = ident
131+
.span()
132+
.join(stream.span())
133+
.unwrap_or_else(|| ident.span());
134+
135+
match kw.as_str() {
136+
"mint" => ConstraintToken::AssociatedTokenMint(Context::new(
137+
span,
138+
ConstraintTokenMint {
139+
mint: stream.parse()?,
140+
},
141+
)),
142+
"authority" => ConstraintToken::AssociatedTokenAuthority(Context::new(
143+
span,
144+
ConstraintTokenAuthority {
145+
auth: stream.parse()?,
146+
},
147+
)),
148+
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
149+
}
150+
}
124151
"bump" => {
125152
let bump = {
126153
if stream.peek(Token![=]) {
@@ -246,6 +273,8 @@ pub struct ConstraintGroupBuilder<'ty> {
246273
pub address: Option<Context<ConstraintAddress>>,
247274
pub token_mint: Option<Context<ConstraintTokenMint>>,
248275
pub token_authority: Option<Context<ConstraintTokenAuthority>>,
276+
pub associated_token_mint: Option<Context<ConstraintTokenMint>>,
277+
pub associated_token_authority: Option<Context<ConstraintTokenAuthority>>,
249278
pub mint_authority: Option<Context<ConstraintMintAuthority>>,
250279
pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
251280
pub bump: Option<Context<ConstraintTokenBump>>,
@@ -273,6 +302,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
273302
address: None,
274303
token_mint: None,
275304
token_authority: None,
305+
associated_token_mint: None,
306+
associated_token_authority: None,
276307
mint_authority: None,
277308
mint_decimals: None,
278309
bump: None,
@@ -307,7 +338,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
307338
// When initializing a non-PDA account, the account being
308339
// initialized must sign to invoke the system program's create
309340
// account instruction.
310-
if self.signer.is_none() && self.seeds.is_none() {
341+
if self.signer.is_none() && self.seeds.is_none() && self.associated_token_mint.is_none()
342+
{
311343
self.signer
312344
.replace(Context::new(i.span(), ConstraintSigner {}));
313345
}
@@ -425,6 +457,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
425457
address,
426458
token_mint,
427459
token_authority,
460+
associated_token_mint,
461+
associated_token_authority,
428462
mint_authority,
429463
mint_decimals,
430464
bump,
@@ -469,6 +503,17 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
469503
)),
470504
},
471505
}
506+
} else if let Some(tm) = &associated_token_mint {
507+
InitKind::AssociatedToken {
508+
mint: tm.clone().into_inner().mint,
509+
owner: match &associated_token_authority {
510+
Some(a) => a.clone().into_inner().auth,
511+
None => return Err(ParseError::new(
512+
tm.span(),
513+
"authority must be provided to initialize a token program derived address"
514+
)),
515+
},
516+
}
472517
} else if let Some(d) = &mint_decimals {
473518
InitKind::Mint {
474519
decimals: d.clone().into_inner().decimals,
@@ -522,6 +567,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
522567
ConstraintToken::Address(c) => self.add_address(c),
523568
ConstraintToken::TokenAuthority(c) => self.add_token_authority(c),
524569
ConstraintToken::TokenMint(c) => self.add_token_mint(c),
570+
ConstraintToken::AssociatedTokenAuthority(c) => self.add_associated_token_authority(c),
571+
ConstraintToken::AssociatedTokenMint(c) => self.add_associated_token_mint(c),
525572
ConstraintToken::MintAuthority(c) => self.add_mint_authority(c),
526573
ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c),
527574
ConstraintToken::Bump(c) => self.add_bump(c),
@@ -585,6 +632,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
585632
if self.token_mint.is_some() {
586633
return Err(ParseError::new(c.span(), "token mint already provided"));
587634
}
635+
if self.associated_token_mint.is_some() {
636+
return Err(ParseError::new(
637+
c.span(),
638+
"associated token mint already provided",
639+
));
640+
}
588641
if self.init.is_none() {
589642
return Err(ParseError::new(
590643
c.span(),
@@ -595,6 +648,26 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
595648
Ok(())
596649
}
597650

651+
fn add_associated_token_mint(&mut self, c: Context<ConstraintTokenMint>) -> ParseResult<()> {
652+
if self.associated_token_mint.is_some() {
653+
return Err(ParseError::new(
654+
c.span(),
655+
"associated token mint already provided",
656+
));
657+
}
658+
if self.token_mint.is_some() {
659+
return Err(ParseError::new(c.span(), "token mint already provided"));
660+
}
661+
if self.init.is_none() {
662+
return Err(ParseError::new(
663+
c.span(),
664+
"init must be provided before token",
665+
));
666+
}
667+
self.associated_token_mint.replace(c);
668+
Ok(())
669+
}
670+
598671
fn add_bump(&mut self, c: Context<ConstraintTokenBump>) -> ParseResult<()> {
599672
if self.bump.is_some() {
600673
return Err(ParseError::new(c.span(), "bump already provided"));
@@ -626,6 +699,32 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
626699
Ok(())
627700
}
628701

702+
fn add_associated_token_authority(
703+
&mut self,
704+
c: Context<ConstraintTokenAuthority>,
705+
) -> ParseResult<()> {
706+
if self.associated_token_authority.is_some() {
707+
return Err(ParseError::new(
708+
c.span(),
709+
"associated token authority already provided",
710+
));
711+
}
712+
if self.token_authority.is_some() {
713+
return Err(ParseError::new(
714+
c.span(),
715+
"token authority already provided",
716+
));
717+
}
718+
if self.init.is_none() {
719+
return Err(ParseError::new(
720+
c.span(),
721+
"init must be provided before token authority",
722+
));
723+
}
724+
self.associated_token_authority.replace(c);
725+
Ok(())
726+
}
727+
629728
fn add_mint_authority(&mut self, c: Context<ConstraintMintAuthority>) -> ParseResult<()> {
630729
if self.mint_authority.is_some() {
631730
return Err(ParseError::new(c.span(), "mint authority already provided"));

spl/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ lazy_static = "1.4.0"
1515
serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "1be91f2", version = "0.4.0", features = ["no-entrypoint"] }
1616
solana-program = "=1.7.11"
1717
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
18+
spl-associated-token-account = { version = "1.0.3", features = ["no-entrypoint"] }

spl/src/associated_token.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use anchor_lang::solana_program::account_info::AccountInfo;
2+
use anchor_lang::solana_program::entrypoint::ProgramResult;
3+
use anchor_lang::solana_program::program_error::ProgramError;
4+
use anchor_lang::solana_program::pubkey::Pubkey;
5+
use anchor_lang::{Accounts, CpiContext};
6+
7+
pub use spl_associated_token_account::ID;
8+
9+
pub fn create<'info>(ctx: CpiContext<'_, '_, '_, 'info, Create<'info>>) -> ProgramResult {
10+
let ix = spl_associated_token_account::create_associated_token_account(
11+
ctx.accounts.payer.key,
12+
ctx.accounts.authority.key,
13+
ctx.accounts.mint.key,
14+
);
15+
solana_program::program::invoke_signed(
16+
&ix,
17+
&[
18+
ctx.accounts.payer,
19+
ctx.accounts.associated_token,
20+
ctx.accounts.authority,
21+
ctx.accounts.mint,
22+
ctx.accounts.system_program,
23+
ctx.accounts.token_program,
24+
ctx.accounts.rent,
25+
],
26+
ctx.signer_seeds,
27+
)
28+
}
29+
30+
#[derive(Accounts)]
31+
pub struct Create<'info> {
32+
pub payer: AccountInfo<'info>,
33+
pub associated_token: AccountInfo<'info>,
34+
pub authority: AccountInfo<'info>,
35+
pub mint: AccountInfo<'info>,
36+
pub system_program: AccountInfo<'info>,
37+
pub token_program: AccountInfo<'info>,
38+
pub rent: AccountInfo<'info>,
39+
}
40+
41+
#[derive(Clone)]
42+
pub struct AssociatedToken;
43+
44+
impl anchor_lang::AccountDeserialize for AssociatedToken {
45+
fn try_deserialize(buf: &mut &[u8]) -> Result<Self, ProgramError> {
46+
AssociatedToken::try_deserialize_unchecked(buf)
47+
}
48+
49+
fn try_deserialize_unchecked(_buf: &mut &[u8]) -> Result<Self, ProgramError> {
50+
Ok(AssociatedToken)
51+
}
52+
}
53+
54+
impl anchor_lang::Id for AssociatedToken {
55+
fn id() -> Pubkey {
56+
ID
57+
}
58+
}

spl/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod associated_token;
12
pub mod dex;
23
pub mod mint;
34
pub mod shmem;

tests/misc/programs/misc/src/context.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::account::*;
22
use anchor_lang::prelude::*;
3-
use anchor_spl::token::{Mint, TokenAccount};
3+
use anchor_spl::associated_token::AssociatedToken;
4+
use anchor_spl::token::{Mint, Token, TokenAccount};
45
use misc2::misc2::MyState as Misc2State;
56
use std::mem::size_of;
67

@@ -31,6 +32,23 @@ pub struct TestTokenSeedsInit<'info> {
3132
pub token_program: AccountInfo<'info>,
3233
}
3334

35+
#[derive(Accounts)]
36+
pub struct TestInitAssociatedToken<'info> {
37+
#[account(
38+
init,
39+
payer = payer,
40+
associated_token::mint = mint,
41+
associated_token::authority = payer,
42+
)]
43+
pub token: Account<'info, TokenAccount>,
44+
pub mint: Account<'info, Mint>,
45+
pub payer: Signer<'info>,
46+
pub rent: Sysvar<'info, Rent>,
47+
pub system_program: Program<'info, System>,
48+
pub token_program: Program<'info, Token>,
49+
pub associated_token_program: Program<'info, AssociatedToken>,
50+
}
51+
3452
#[derive(Accounts)]
3553
#[instruction(nonce: u8)]
3654
pub struct TestInstructionConstraint<'info> {

0 commit comments

Comments
 (0)