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

lang: Require zero accounts to be unique #3409

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- cli: Add optional `package-manager` flag in `init` command to set package manager field in Anchor.toml ([#3328](https://github.com/coral-xyz/anchor/pull/3328)).
- cli: Add test template for [Mollusk](https://github.com/buffalojoec/mollusk) ([#3352](https://github.com/coral-xyz/anchor/pull/3352)).
- idl: Disallow account discriminators that can conflict with the `zero` constraint ([#3365](https://github.com/coral-xyz/anchor/pull/3365)).
- cli: Include recommended solana args by default and add new --max-retries ([#3354](https://github.com/coral-xyz/anchor/pull/3354)).
- cli: Include recommended solana args by default and add new `--max-retries` option to the `deploy` command ([#3354](https://github.com/coral-xyz/anchor/pull/3354)).

### Fixes

Expand Down Expand Up @@ -100,6 +100,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- lang: Fix `cpi` feature instructions not accounting for discriminator overrides ([#3376](https://github.com/coral-xyz/anchor/pull/3376)).
- idl: Ignore compiler warnings during builds ([#3396](https://github.com/coral-xyz/anchor/pull/3396)).
- cli: Avoid extra IDL generation during `verify` ([#3398](https://github.com/coral-xyz/anchor/pull/3398)).
- lang: Require `zero` accounts to be unique ([#3409](https://github.com/coral-xyz/anchor/pull/3409)).

### Breaking

Expand Down
49 changes: 47 additions & 2 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ fn generate_constraint(
) -> proc_macro2::TokenStream {
match c {
Constraint::Init(c) => generate_constraint_init(f, c, accs),
Constraint::Zeroed(c) => generate_constraint_zeroed(f, c),
Constraint::Zeroed(c) => generate_constraint_zeroed(f, c, accs),
Constraint::Mut(c) => generate_constraint_mut(f, c),
Constraint::HasOne(c) => generate_constraint_has_one(f, c, accs),
Constraint::Signer(c) => generate_constraint_signer(f, c),
Expand Down Expand Up @@ -197,14 +197,58 @@ pub fn generate_constraint_init(
generate_constraint_init_group(f, c, accs)
}

pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macro2::TokenStream {
pub fn generate_constraint_zeroed(
f: &Field,
_c: &ConstraintZeroed,
accs: &AccountsStruct,
) -> proc_macro2::TokenStream {
let account_ty = f.account_ty();
let discriminator = quote! { #account_ty::DISCRIMINATOR };

let field = &f.ident;
let name_str = field.to_string();
let ty_decl = f.ty_decl(true);
let from_account_info = f.from_account_info(None, false);

// Require `zero` constraint accounts to be unique by:
//
// 1. Getting the names of all accounts that have the `zero` constraint and are declared before
// the current field (in order to avoid checking the same field).
// 2. Comparing the key of the current field with all the previous fields' keys.
// 3. Returning an error if a match is found.
let unique_account_checks = accs
.fields
.iter()
.filter_map(|af| match af {
AccountField::Field(field) => Some(field),
_ => None,
})
.take_while(|field| field.ident != f.ident)
.filter(|field| field.constraints.is_zeroed())
.map(|other_field| {
let other = &other_field.ident;
let err = quote! {
Err(
anchor_lang::error::Error::from(
anchor_lang::error::ErrorCode::ConstraintZero
).with_account_name(#name_str)
)
};
if other_field.is_optional {
quote! {
if #other.is_some() && #field.key == &#other.as_ref().unwrap().key() {
return #err;
}
}
} else {
quote! {
if #field.key == &#other.key() {
return #err;
}
}
}
});

quote! {
let #field: #ty_decl = {
let mut __data: &[u8] = &#field.try_borrow_data()?;
Expand All @@ -213,6 +257,7 @@ pub fn generate_constraint_zeroed(f: &Field, _c: &ConstraintZeroed) -> proc_macr
if __has_disc {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintZero).with_account_name(#name_str));
}
#(#unique_account_checks)*
#from_account_info
};
}
Expand Down
8 changes: 8 additions & 0 deletions tests/misc/programs/misc-optional/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,3 +744,11 @@ pub struct InitManyAssociatedTokenAccounts<'info> {
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
}

#[derive(Accounts)]
pub struct TestMultipleZeroConstraint<'info> {
#[account(zero)]
pub one: Option<Account<'info, Data>>,
#[account(zero)]
pub two: Option<Account<'info, Data>>,
}
4 changes: 4 additions & 0 deletions tests/misc/programs/misc-optional/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,8 @@ pub mod misc_optional {
) -> Result<()> {
Ok(())
}

pub fn test_multiple_zero_constraint(_ctx: Context<TestMultipleZeroConstraint>) -> Result<()> {
Ok(())
}
}
8 changes: 8 additions & 0 deletions tests/misc/programs/misc/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,3 +816,11 @@ pub struct TestBoxedOwnerConstraint<'info> {
#[cfg(feature = "my-feature")]
#[derive(Accounts)]
pub struct Empty {}

#[derive(Accounts)]
pub struct TestMultipleZeroConstraint<'info> {
#[account(zero)]
pub one: Account<'info, Data>,
#[account(zero)]
pub two: Account<'info, Data>,
}
4 changes: 4 additions & 0 deletions tests/misc/programs/misc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,8 @@ pub mod misc {
pub fn only_my_feature(_ctx: Context<Empty>) -> Result<()> {
Ok(())
}

pub fn test_multiple_zero_constraint(_ctx: Context<TestMultipleZeroConstraint>) -> Result<()> {
Ok(())
}
}
43 changes: 43 additions & 0 deletions tests/misc/tests/misc/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3240,6 +3240,49 @@ const miscTest = (
assert.isDefined(thisTx);
});
});

describe("Multiple `zero` constraint", () => {
it("Passing different accounts works", async () => {
const oneKp = anchor.web3.Keypair.generate();
const twoKp = anchor.web3.Keypair.generate();
await program.methods
.testMultipleZeroConstraint()
.preInstructions(
await Promise.all([
program.account.data.createInstruction(oneKp),
program.account.data.createInstruction(twoKp),
])
)
.accounts({ one: oneKp.publicKey, two: twoKp.publicKey })
.signers([oneKp, twoKp])
.rpc();
});

it("Passing the same account throws", async () => {
const oneKp = anchor.web3.Keypair.generate();
try {
await program.methods
.testMultipleZeroConstraint()
.preInstructions([
await program.account.data.createInstruction(oneKp),
])
.accounts({
one: oneKp.publicKey,
two: oneKp.publicKey,
})
.signers([oneKp])
.rpc();
throw new Error("Transaction did not fail!");
} catch (e) {
assert(e instanceof AnchorError);
const err: AnchorError = e;
assert.strictEqual(
err.error.errorCode.number,
anchor.LangErrorCode.ConstraintZero
);
}
});
});
};
};

Expand Down
Loading