Skip to content

Commit

Permalink
lang: Require zero accounts to be unique (#3409)
Browse files Browse the repository at this point in the history
  • Loading branch information
acheroncrypto authored Dec 10, 2024
1 parent 8bc883a commit 955e7ea
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 3 deletions.
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

0 comments on commit 955e7ea

Please sign in to comment.