Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add seeds::program constraint for PDAs. #1197

Merged
merged 19 commits into from Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,10 @@ incremented for features.

## [Unreleased]

### Features

* lang: Add `seeds::program` constraint for specifying which program_id to use when deriving PDAs.([#1197](https://github.com/project-serum/anchor/pull/1197))

### Breaking

* lang: rename `loader_account` module to `account_loader` module ([#1279](https://github.com/project-serum/anchor/pull/1279))
Expand Down
8 changes: 4 additions & 4 deletions examples/tutorial/yarn.lock
Expand Up @@ -30,10 +30,10 @@
"@ethersproject/logger" "^5.5.0"
hash.js "1.1.7"

"@project-serum/anchor@^0.19.0":
version "0.19.0"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.19.0.tgz#79f1fbe7c3134860ccbfe458a0e09daf79644885"
integrity sha512-cs0LBmJOrL9eJ8MRNqitnzbpCT5QEzVdJmiIjfNV5YaGn1K9vISR7DtISj3Bdl3KBdLqii4CTw1mpHdi8iXUCg==
"@project-serum/anchor@^0.20.0":
version "0.20.0"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.20.0.tgz#547f5c0ff7e66809fa7118b2e3abd8087b5ec519"
integrity sha512-p1KOiqGBIbNsopMrSVoPwgxR1iPffsdjMNCOysahTPL9whX2CLX9HQCdopHjYaGl7+SdHRuXml6Wahk/wUmC8g==
dependencies:
"@project-serum/borsh" "^0.2.2"
"@solana/web3.js" "^1.17.0"
Expand Down
37 changes: 30 additions & 7 deletions lang/derive/accounts/src/lib.rs
Expand Up @@ -159,7 +159,9 @@ use syn::parse_macro_input;
/// you can pass it in as instruction data and set the bump value like shown in the example,
/// using the <code>instruction_data</code> attribute.
/// Anchor will then check that the bump returned by <code>find_program_address</code> equals
/// the bump in the instruction data.
/// the bump in the instruction data.<br>
/// <code>seeds::program</code> cannot be used together with init because the creation of an
/// account requires its signature which for PDAs only the currently executing program can provide.
/// </li>
/// </ul>
/// Example:
Expand Down Expand Up @@ -228,21 +230,42 @@ use syn::parse_macro_input;
/// <tr>
/// <td>
/// <code>#[account(seeds = &lt;seeds&gt;, bump)]</code><br><br>
/// <code>#[account(seeds = &lt;seeds&gt;, bump = &lt;expr&gt;)]</code>
/// <code>#[account(seeds = &lt;seeds&gt;, bump, seeds::program = &lt;expr&gt;)]<br><br>
/// <code>#[account(seeds = &lt;seeds&gt;, bump = &lt;expr&gt;)]</code><br><br>
/// <code>#[account(seeds = &lt;seeds&gt;, bump = &lt;expr&gt;, seeds::program = &lt;expr&gt;)]</code><br><br>
/// </td>
/// <td>
/// Checks that given account is a PDA derived from the currently executing program,
/// the seeds, and if provided, the bump. If not provided, anchor uses the canonical
/// bump. Will be adjusted in the future to allow PDA to be derived from other programs.<br>
/// bump. <br>
/// Add <code>seeds::program = &lt;expr&gt;</code> to derive the PDA from a different
/// program than the currently executing one.<br>
/// This constraint behaves slightly differently when used with <code>init</code>.
/// See its description.
/// <br><br>
/// Example:
/// <pre><code>
/// #[account(seeds = [b"example_seed], bump)]
/// pub canonical_pda: AccountInfo<'info>,
/// #[account(seeds = [b"other_seed], bump = 142)]
/// pub arbitrary_pda: AccountInfo<'info>
/// #[derive(Accounts)]
/// #[instruction(first_bump: u8, second_bump: u8)]
/// pub struct Example {
/// #[account(seeds = [b"example_seed], bump)]
/// pub canonical_pda: AccountInfo<'info>,
/// #[account(
/// seeds = [b"example_seed],
/// bump,
/// seeds::program = other_program.key()
/// )]
/// pub canonical_pda_two: AccountInfo<'info>,
/// #[account(seeds = [b"other_seed], bump = first_bump)]
/// pub arbitrary_pda: AccountInfo<'info>
/// #[account(
/// seeds = [b"other_seed],
/// bump = second_bump,
/// seeds::program = other_program.key()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit ugly now that I look at this next to seeds, bump. We'll want to rethink this in the future. Potentially just solved by the constraint grouping work, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea once we group them it can just be program cause it will be clear which constraint it belongs to

/// )]
/// pub arbitrary_pda_two: AccountInfo<'info>,
/// pub other_program: Program<'info, OtherProgram>
/// }
/// </code></pre>
/// </td>
/// </tr>
Expand Down
15 changes: 12 additions & 3 deletions lang/syn/src/codegen/accounts/constraints.rs
Expand Up @@ -327,6 +327,15 @@ fn generate_constraint_init_group(f: &Field, c: &ConstraintInitGroup) -> proc_ma
fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2::TokenStream {
let name = &f.ident;
let s = &mut c.seeds.clone();

let deriving_program_id = c
.program_seed
.clone()
// If they specified a seeds::program to use when deriving the PDA, use it.
.map(|program_id| quote! { #program_id })
// Otherwise fall back to the current program's program_id.
.unwrap_or(quote! { program_id });

// If the seeds came with a trailing comma, we need to chop it off
// before we interpolate them below.
if let Some(pair) = s.pop() {
Expand All @@ -340,7 +349,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
quote! {
let (__program_signer, __bump) = anchor_lang::solana_program::pubkey::Pubkey::find_program_address(
&[#s],
program_id,
&#deriving_program_id,
);
if #name.key() != __program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
Expand All @@ -362,7 +371,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
&[
Pubkey::find_program_address(
&[#s],
program_id,
&#deriving_program_id,
).1
][..]
]
Expand All @@ -378,7 +387,7 @@ fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2
quote! {
let __program_signer = Pubkey::create_program_address(
&#seeds[..],
program_id,
&#deriving_program_id,
).map_err(|_| anchor_lang::__private::ErrorCode::ConstraintSeeds)?;
if #name.key() != __program_signer {
return Err(anchor_lang::__private::ErrorCode::ConstraintSeeds.into());
Expand Down
9 changes: 8 additions & 1 deletion lang/syn/src/lib.rs
Expand Up @@ -610,6 +610,7 @@ pub enum ConstraintToken {
MintFreezeAuthority(Context<ConstraintMintFreezeAuthority>),
MintDecimals(Context<ConstraintMintDecimals>),
Bump(Context<ConstraintTokenBump>),
ProgramSeed(Context<ConstraintProgramSeed>),
}

impl Parse for ConstraintToken {
Expand Down Expand Up @@ -687,7 +688,8 @@ pub struct ConstraintInitGroup {
pub struct ConstraintSeedsGroup {
pub is_init: bool,
pub seeds: Punctuated<Expr, Token![,]>,
pub bump: Option<Expr>, // None => bump was given without a target.
pub bump: Option<Expr>, // None => bump was given without a target.
pub program_seed: Option<Expr>, // None => use the current program's program_id
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -771,6 +773,11 @@ pub struct ConstraintTokenBump {
bump: Option<Expr>,
}

#[derive(Debug, Clone)]
pub struct ConstraintProgramSeed {
program_seed: Expr,
}

#[derive(Debug, Clone)]
pub struct ConstraintAssociatedToken {
pub wallet: Expr,
Expand Down
79 changes: 69 additions & 10 deletions lang/syn/src/parser/accounts/constraints.rs
Expand Up @@ -182,6 +182,43 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
};
ConstraintToken::Bump(Context::new(ident.span(), ConstraintTokenBump { bump }))
}
"seeds" => {
if stream.peek(Token![:]) {
stream.parse::<Token![:]>()?;
stream.parse::<Token![:]>()?;
let kw = stream.call(Ident::parse_any)?.to_string();
stream.parse::<Token![=]>()?;

let span = ident
.span()
.join(stream.span())
.unwrap_or_else(|| ident.span());

match kw.as_str() {
"program" => ConstraintToken::ProgramSeed(Context::new(
span,
ConstraintProgramSeed {
program_seed: stream.parse()?,
},
)),
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
} else {
stream.parse::<Token![=]>()?;
let span = ident
.span()
.join(stream.span())
.unwrap_or_else(|| ident.span());
let seeds;
let bracket = bracketed!(seeds in stream);
ConstraintToken::Seeds(Context::new(
span.join(bracket.span).unwrap_or(span),
ConstraintSeeds {
seeds: seeds.parse_terminated(Expr::parse)?,
},
))
}
}
_ => {
stream.parse::<Token![=]>()?;
let span = ident
Expand Down Expand Up @@ -234,16 +271,6 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
space: stream.parse()?,
},
)),
"seeds" => {
let seeds;
let bracket = bracketed!(seeds in stream);
ConstraintToken::Seeds(Context::new(
span.join(bracket.span).unwrap_or(span),
ConstraintSeeds {
seeds: seeds.parse_terminated(Expr::parse)?,
},
))
}
"constraint" => ConstraintToken::Raw(Context::new(
span,
ConstraintRaw {
Expand Down Expand Up @@ -308,6 +335,7 @@ pub struct ConstraintGroupBuilder<'ty> {
pub mint_freeze_authority: Option<Context<ConstraintMintFreezeAuthority>>,
pub mint_decimals: Option<Context<ConstraintMintDecimals>>,
pub bump: Option<Context<ConstraintTokenBump>>,
pub program_seed: Option<Context<ConstraintProgramSeed>>,
}

impl<'ty> ConstraintGroupBuilder<'ty> {
Expand Down Expand Up @@ -338,6 +366,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_freeze_authority: None,
mint_decimals: None,
bump: None,
program_seed: None,
}
}

Expand Down Expand Up @@ -494,6 +523,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
mint_freeze_authority,
mint_decimals,
bump,
program_seed,
} = self;

// Converts Option<Context<T>> -> Option<T>.
Expand All @@ -519,6 +549,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
bump: into_inner!(bump)
.map(|b| b.bump)
.expect("bump must be provided with seeds"),
program_seed: into_inner!(program_seed).map(|id| id.program_seed),
});
let associated_token = match (associated_token_mint, associated_token_authority) {
(Some(mint), Some(auth)) => Some(ConstraintAssociatedToken {
Expand Down Expand Up @@ -620,6 +651,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
ConstraintToken::MintFreezeAuthority(c) => self.add_mint_freeze_authority(c),
ConstraintToken::MintDecimals(c) => self.add_mint_decimals(c),
ConstraintToken::Bump(c) => self.add_bump(c),
ConstraintToken::ProgramSeed(c) => self.add_program_seed(c),
}
}

Expand Down Expand Up @@ -725,6 +757,33 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
Ok(())
}

fn add_program_seed(&mut self, c: Context<ConstraintProgramSeed>) -> ParseResult<()> {
if self.program_seed.is_some() {
return Err(ParseError::new(c.span(), "seeds::program already provided"));
}
if self.seeds.is_none() {
return Err(ParseError::new(
c.span(),
"seeds must be provided before seeds::program",
));
}
if let Some(ref init) = self.init {
if init.if_needed {
return Err(ParseError::new(
c.span(),
"seeds::program cannot be used with init_if_needed",
));
} else {
return Err(ParseError::new(
c.span(),
"seeds::program cannot be used with init",
));
}
}
self.program_seed.replace(c);
Ok(())
}

fn add_token_authority(&mut self, c: Context<ConstraintTokenAuthority>) -> ParseResult<()> {
if self.token_authority.is_some() {
return Err(ParseError::new(
Expand Down
3 changes: 3 additions & 0 deletions tests/misc/package.json
Expand Up @@ -15,5 +15,8 @@
},
"scripts": {
"test": "anchor test"
},
"dependencies": {
"mocha": "^9.1.3"
}
}
22 changes: 22 additions & 0 deletions tests/misc/programs/misc/src/context.rs
Expand Up @@ -379,3 +379,25 @@ pub struct InitIfNeededChecksRentExemption<'info> {
pub system_program: Program<'info, System>
}

#[derive(Accounts)]
#[instruction(bump: u8, second_bump: u8)]
pub struct TestProgramIdConstraint<'info> {
armaniferrante marked this conversation as resolved.
Show resolved Hide resolved
// not a real associated token account
// just deriving like this for testing purposes
#[account(seeds = [b"seed"], bump = bump, seeds::program = anchor_spl::associated_token::ID)]
first: AccountInfo<'info>,

#[account(seeds = [b"seed"], bump = second_bump, seeds::program = crate::ID)]
second: AccountInfo<'info>,
}

#[derive(Accounts)]
pub struct TestProgramIdConstraintUsingFindPda<'info> {
// not a real associated token account
// just deriving like this for testing purposes
#[account(seeds = [b"seed"], bump, seeds::program = anchor_spl::associated_token::ID)]
first: AccountInfo<'info>,

#[account(seeds = [b"seed"], bump, seeds::program = crate::ID)]
second: AccountInfo<'info>,
}
14 changes: 14 additions & 0 deletions tests/misc/programs/misc/src/lib.rs
Expand Up @@ -268,4 +268,18 @@ pub mod misc {
pub fn init_if_needed_checks_rent_exemption(_ctx: Context<InitIfNeededChecksRentExemption>) -> ProgramResult {
Ok(())
}

pub fn test_program_id_constraint(
_ctx: Context<TestProgramIdConstraint>,
_bump: u8,
_second_bump: u8
) -> ProgramResult {
Ok(())
}

pub fn test_program_id_constraint_find_pda(
_ctx: Context<TestProgramIdConstraintUsingFindPda>,
) -> ProgramResult {
Ok(())
}
}