Skip to content

Commit d790f27

Browse files
authored
Fix: Relax duplicate mutable constraint (#3946) (#4202)
* lang: Update duplicate mutable account validation logic to include only types that serialize on exit (#3946) * lang: Update documentation for `dup` constraint * chore: Fix PR link * lang: Update duplicate mutable account constraint to include `Migration` type * lang: Allow duplicates in remaining accounts * fix: use strict AnchorError assertion in nested duplicate test * lang: Introduce `DuplicateMutableAccountKeys` trait for composite account validation
1 parent 5d05830 commit d790f27

File tree

12 files changed

+374
-197
lines changed

12 files changed

+374
-197
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The minor version will be incremented upon a breaking change and the patch versi
2828
- idl: Fix `address` constraint not resolving constants that have numbers in their identifiers ([#4144](https://github.com/solana-foundation/anchor/pull/4144)).
2929
- lang: Fix constant nested string generation in `declare_program!` ([#4158](https://github.com/solana-foundation/anchor/pull/4158)).
3030
- idl: Fix `local_file` method not found for `proc_macro2::Span` error ([#4187](https://github.com/solana-foundation/anchor/pull/4187)).
31+
- lang: Relax duplicate mutable account constraint to only check types that serialize on exit (`Account`, `LazyAccount`, `InterfaceAccount`, `Migration`) ([#4202](https://github.com/solana-foundation/anchor/pull/4202)).
3132

3233
### Breaking
3334

bench/BINARY_SIZE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ The programs and their tests are located in [/tests/bench](https://github.com/co
1616

1717
Solana version: 2.3.0
1818

19-
| Program | Binary Size | - |
20-
| ------- | ----------- | ----------------------- |
21-
| bench | 1,015,624 | 🟢 **-111,216 (9.87%)** |
19+
| Program | Binary Size | - |
20+
| ------- | ----------- | ------------------------ |
21+
| bench | 932,992 | 🟢 **-193,848 (17.20%)** |
2222

2323
### Notable changes
2424

bench/COMPUTE_UNITS.md

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,93 +18,93 @@ Solana version: 2.3.0
1818

1919
| Instruction | Compute Units | - |
2020
| --------------------------- | ------------- | --------------------- |
21-
| accountInfo1 | 760 | 🔴 **+75 (10.95%)** |
22-
| accountInfo2 | 1,180 | 🔴 **+127 (12.06%)** |
23-
| accountInfo4 | 1,952 | 🔴 **+202 (11.54%)** |
24-
| accountInfo8 | 3,530 | 🔴 **+395 (12.60%)** |
21+
| accountInfo1 | 702 | 🔴 **+17 (2.48%)** |
22+
| accountInfo2 | 1,115 | 🔴 **+62 (5.89%)** |
23+
| accountInfo4 | 1,921 | 🔴 **+171 (9.77%)** |
24+
| accountInfo8 | 3,480 | 🔴 **+345 (11.00%)** |
2525
| accountEmptyInit1 | 4,770 | 🟢 **-145 (2.95%)** |
26-
| accountEmpty1 | 773 | 🟢 **-1 (0.13%)** |
26+
| accountEmpty1 | 738 | 🟢 **-36 (4.65%)** |
2727
| accountEmptyInit2 | 8,487 | 🟢 **-306 (3.48%)** |
28-
| accountEmpty2 | 1,173 | 🟢 **-1 (0.09%)** |
28+
| accountEmpty2 | 1,138 | 🟢 **-36 (3.07%)** |
2929
| accountEmptyInit4 | 15,915 | 🟢 **-833 (4.97%)** |
30-
| accountEmpty4 | 1,960 | 🟢 **-2 (0.10%)** |
30+
| accountEmpty4 | 1,923 | 🟢 **-39 (1.99%)** |
3131
| accountEmptyInit8 | 30,779 | 🟢 **-1,578 (4.88%)** |
32-
| accountEmpty8 | 3,539 | 🟢 **-9 (0.25%)** |
32+
| accountEmpty8 | 3,500 | 🟢 **-48 (1.35%)** |
3333
| accountSizedInit1 | 4,864 | 🟢 **-155 (3.09%)** |
34-
| accountSized1 | 820 | 🟢 **-2 (0.24%)** |
34+
| accountSized1 | 786 | 🟢 **-36 (4.38%)** |
3535
| accountSizedInit2 | 8,654 | 🟢 **-327 (3.64%)** |
36-
| accountSized2 | 1,236 | 🟢 **-4 (0.32%)** |
36+
| accountSized2 | 1,198 | 🟢 **-42 (3.39%)** |
3737
| accountSizedInit4 | 16,236 | 🟢 **-918 (5.35%)** |
38-
| accountSized4 | 2,070 | 🟢 **-12 (0.58%)** |
38+
| accountSized4 | 2,031 | 🟢 **-51 (2.45%)** |
3939
| accountSizedInit8 | 31,363 | 🟢 **-1,650 (5.00%)** |
40-
| accountSized8 | 3,731 | 🟢 **-31 (0.82%)** |
40+
| accountSized8 | 3,694 | 🟢 **-68 (1.81%)** |
4141
| accountUnsizedInit1 | 4,967 | 🟢 **-160 (3.12%)** |
42-
| accountUnsized1 | 854 | 🟢 **-20 (2.29%)** |
42+
| accountUnsized1 | 814 | 🟢 **-60 (6.86%)** |
4343
| accountUnsizedInit2 | 8,841 | 🟢 **-410 (4.43%)** |
44-
| accountUnsized2 | 1,281 | 🟢 **-45 (3.39%)** |
44+
| accountUnsized2 | 1,240 | 🟢 **-86 (6.49%)** |
4545
| accountUnsizedInit4 | 16,559 | 🟢 **-819 (4.71%)** |
46-
| accountUnsized4 | 2,134 | 🟢 **-97 (4.35%)** |
46+
| accountUnsized4 | 2,093 | 🟢 **-138 (6.19%)** |
4747
| accountUnsizedInit8 | 31,985 | 🟢 **-1,976 (5.82%)** |
48-
| accountUnsized8 | 3,837 | 🟢 **-198 (4.91%)** |
48+
| accountUnsized8 | 3,797 | 🟢 **-238 (5.90%)** |
4949
| boxedAccountEmptyInit1 | 4,864 | 🟢 **-143 (2.86%)** |
50-
| boxedAccountEmpty1 | 864 | - |
50+
| boxedAccountEmpty1 | 831 | 🟢 **-33 (3.82%)** |
5151
| boxedAccountEmptyInit2 | 8,604 | 🟢 **-302 (3.39%)** |
52-
| boxedAccountEmpty2 | 1,285 | 🟢 **-1 (0.08%)** |
52+
| boxedAccountEmpty2 | 1,253 | 🟢 **-33 (2.57%)** |
5353
| boxedAccountEmptyInit4 | 16,075 | 🟢 **-827 (4.89%)** |
54-
| boxedAccountEmpty4 | 2,112 | 🟢 **-3 (0.14%)** |
54+
| boxedAccountEmpty4 | 2,077 | 🟢 **-38 (1.80%)** |
5555
| boxedAccountEmptyInit8 | 31,026 | 🟢 **-1,565 (4.80%)** |
5656
| boxedAccountEmpty8 | 3,797 | 🟢 **-4 (0.11%)** |
5757
| boxedAccountSizedInit1 | 4,952 | 🟢 **-151 (2.96%)** |
58-
| boxedAccountSized1 | 913 | 🔴 **+1 (0.11%)** |
58+
| boxedAccountSized1 | 877 | 🟢 **-35 (3.84%)** |
5959
| boxedAccountSizedInit2 | 8,756 | 🟢 **-319 (3.52%)** |
60-
| boxedAccountSized2 | 1,351 | 🟢 **-4 (0.30%)** |
60+
| boxedAccountSized2 | 1,318 | 🟢 **-37 (2.73%)** |
6161
| boxedAccountSizedInit4 | 16,355 | 🟢 **-859 (4.99%)** |
62-
| boxedAccountSized4 | 2,218 | 🟢 **-13 (0.58%)** |
62+
| boxedAccountSized4 | 2,185 | 🟢 **-46 (2.06%)** |
6363
| boxedAccountSizedInit8 | 31,562 | 🟢 **-1,959 (5.84%)** |
6464
| boxedAccountSized8 | 3,984 | 🟢 **-23 (0.57%)** |
6565
| boxedAccountUnsizedInit1 | 5,044 | 🟢 **-158 (3.04%)** |
66-
| boxedAccountUnsized1 | 943 | 🟢 **-21 (2.18%)** |
66+
| boxedAccountUnsized1 | 907 | 🟢 **-57 (5.91%)** |
6767
| boxedAccountUnsizedInit2 | 8,916 | 🟢 **-335 (3.62%)** |
68-
| boxedAccountUnsized2 | 1,386 | 🟢 **-48 (3.35%)** |
68+
| boxedAccountUnsized2 | 1,352 | 🟢 **-82 (5.72%)** |
6969
| boxedAccountUnsizedInit4 | 16,651 | 🟢 **-891 (5.08%)** |
70-
| boxedAccountUnsized4 | 2,270 | 🟢 **-97 (4.10%)** |
70+
| boxedAccountUnsized4 | 2,234 | 🟢 **-133 (5.62%)** |
7171
| boxedAccountUnsizedInit8 | 32,130 | 🟢 **-2,023 (5.92%)** |
7272
| boxedAccountUnsized8 | 4,063 | 🟢 **-194 (4.56%)** |
73-
| boxedInterfaceAccountMint1 | 1,128 | 🔴 **+18 (1.62%)** |
74-
| boxedInterfaceAccountMint2 | 1,523 | 🟢 **-11 (0.72%)** |
75-
| boxedInterfaceAccountMint4 | 2,307 | 🟢 **-63 (2.66%)** |
73+
| boxedInterfaceAccountMint1 | 1,090 | 🟢 **-20 (1.80%)** |
74+
| boxedInterfaceAccountMint2 | 1,486 | 🟢 **-48 (3.13%)** |
75+
| boxedInterfaceAccountMint4 | 2,268 | 🟢 **-102 (4.30%)** |
7676
| boxedInterfaceAccountMint8 | 3,907 | 🟢 **-157 (3.86%)** |
77-
| boxedInterfaceAccountToken1 | 1,255 | 🔴 **+9 (0.72%)** |
78-
| boxedInterfaceAccountToken2 | 1,765 | 🟢 **-29 (1.62%)** |
79-
| boxedInterfaceAccountToken4 | 2,779 | 🟢 **-99 (3.44%)** |
77+
| boxedInterfaceAccountToken1 | 1,217 | 🟢 **-29 (2.33%)** |
78+
| boxedInterfaceAccountToken2 | 1,728 | 🟢 **-66 (3.68%)** |
79+
| boxedInterfaceAccountToken4 | 2,740 | 🟢 **-138 (4.79%)** |
8080
| boxedInterfaceAccountToken8 | 4,839 | 🟢 **-229 (4.52%)** |
81-
| interfaceAccountMint1 | 1,145 | 🔴 **+19 (1.69%)** |
82-
| interfaceAccountMint2 | 1,533 | 🟢 **-29 (1.86%)** |
83-
| interfaceAccountMint4 | 2,313 | 🟢 **-119 (4.89%)** |
81+
| interfaceAccountMint1 | 1,105 | 🟢 **-21 (1.87%)** |
82+
| interfaceAccountMint2 | 1,490 | 🟢 **-72 (4.61%)** |
83+
| interfaceAccountMint4 | 2,268 | 🟢 **-164 (6.74%)** |
8484
| interfaceAccountMint8 | 3,835 | 🟢 **-328 (7.88%)** |
85-
| interfaceAccountToken1 | 1,279 | 🔴 **+11 (0.87%)** |
86-
| interfaceAccountToken2 | 1,792 | 🟢 **-57 (3.08%)** |
87-
| interfaceAccountToken4 | 2,825 | 🟢 **-172 (5.74%)** |
88-
| interface1 | 918 | 🔴 **+40 (4.56%)** |
89-
| interface2 | 1,064 | 🔴 **+41 (4.01%)** |
90-
| interface4 | 1,345 | 🔴 **+44 (3.38%)** |
91-
| interface8 | 1,912 | 🔴 **+45 (2.41%)** |
92-
| program1 | 934 | 🔴 **+44 (4.94%)** |
93-
| program2 | 1,084 | 🔴 **+49 (4.73%)** |
94-
| program4 | 1,373 | 🔴 **+60 (4.57%)** |
95-
| program8 | 1,956 | 🔴 **+77 (4.10%)** |
96-
| signer1 | 923 | 🔴 **+49 (5.61%)** |
97-
| signer2 | 1,272 | 🔴 **+99 (8.44%)** |
98-
| signer4 | 1,957 | 🔴 **+198 (11.26%)** |
99-
| signer8 | 3,332 | 🔴 **+391 (13.29%)** |
100-
| systemAccount1 | 945 | 🔴 **+34 (3.73%)** |
101-
| systemAccount2 | 1,304 | 🔴 **+69 (5.59%)** |
102-
| systemAccount4 | 2,009 | 🔴 **+138 (7.38%)** |
103-
| systemAccount8 | 3,424 | 🔴 **+271 (8.59%)** |
104-
| uncheckedAccount1 | 931 | 🔴 **+49 (5.56%)** |
105-
| uncheckedAccount2 | 1,263 | 🔴 **+101 (8.69%)** |
106-
| uncheckedAccount4 | 1,911 | 🔴 **+195 (11.36%)** |
107-
| uncheckedAccount8 | 3,207 | 🔴 **+374 (13.20%)** |
85+
| interfaceAccountToken1 | 1,237 | 🟢 **-31 (2.44%)** |
86+
| interfaceAccountToken2 | 1,749 | 🟢 **-100 (5.41%)** |
87+
| interfaceAccountToken4 | 2,775 | 🟢 **-222 (7.41%)** |
88+
| interface1 | 883 | 🔴 **+5 (0.57%)** |
89+
| interface2 | 1,029 | 🔴 **+6 (0.59%)** |
90+
| interface4 | 1,308 | 🔴 **+7 (0.54%)** |
91+
| interface8 | 1,873 | 🔴 **+6 (0.32%)** |
92+
| program1 | 899 | 🔴 **+9 (1.01%)** |
93+
| program2 | 1,049 | 🔴 **+14 (1.35%)** |
94+
| program4 | 1,336 | 🔴 **+23 (1.75%)** |
95+
| program8 | 1,917 | 🔴 **+38 (2.02%)** |
96+
| signer1 | 888 | 🔴 **+14 (1.60%)** |
97+
| signer2 | 1,237 | 🔴 **+64 (5.46%)** |
98+
| signer4 | 1,920 | 🔴 **+161 (9.15%)** |
99+
| signer8 | 3,293 | 🔴 **+352 (11.97%)** |
100+
| systemAccount1 | 910 | 🟢 **-1 (0.11%)** |
101+
| systemAccount2 | 1,269 | 🔴 **+34 (2.75%)** |
102+
| systemAccount4 | 1,972 | 🔴 **+101 (5.40%)** |
103+
| systemAccount8 | 3,385 | 🔴 **+232 (7.36%)** |
104+
| uncheckedAccount1 | 896 | 🔴 **+14 (1.59%)** |
105+
| uncheckedAccount2 | 1,227 | 🔴 **+65 (5.59%)** |
106+
| uncheckedAccount4 | 1,875 | 🔴 **+159 (9.27%)** |
107+
| uncheckedAccount8 | 3,171 | 🔴 **+338 (11.93%)** |
108108

109109
### Notable changes
110110

docs/content/docs/references/account-constraints.mdx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,14 @@ Examples: [Github](https://github.com/solana-developers/anchor-examples/tree/mai
4040

4141
### `#[account(dup)]`
4242

43-
Description: By default, Anchor will prevents duplicate mutable accounts to avoid potential security issues and unintended behavior.
44-
The `dup` constraint explicitly allows this for cases where it's intentional and safe.
43+
Description: By default, Anchor prevents duplicate mutable accounts to avoid
44+
potential security issues and unintended behavior. The `dup` constraint
45+
explicitly allows this for cases where it's intentional and safe.
4546

46-
**Note**: This constraint only applies to mutable accounts (`mut`). Readonly accounts naturally allow duplicates without requiring the `dup` constraint.
47+
**Note**: This constraint only applies to mutable account (`mut`) types that
48+
serialize on exit. Other types like `UncheckedAccount`, `Signer`,
49+
`SystemAccount`, `AccountLoader`, `Program`, and `Interface` naturally allow
50+
duplicates as they don't serialize data on exit.
4751

4852
```rust title="attribute"
4953
#[account(mut, dup)]
@@ -330,7 +334,8 @@ Description: Create or validate close authority extension on the mint account.
330334

331335
### `#[account(extensions::permanent_delegate::*)]`
332336

333-
Description: Create or validate permanent delegate extension on the mint account.
337+
Description: Create or validate permanent delegate extension on the mint
338+
account.
334339

335340
```rust title="attribute"
336341
#[account(
@@ -362,7 +367,8 @@ Description: Create or validate group pointer extension on the mint account.
362367

363368
### `#[account(extensions::group_member_pointer::*)]`
364369

365-
Description: Create or validate group member pointer extension on the mint account.
370+
Description: Create or validate group member pointer extension on the mint
371+
account.
366372

367373
```rust title="attribute"
368374
#[account(

lang/derive/accounts/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ use syn::parse_macro_input;
9696
/// Allows the same mutable account to be passed multiple times within the same instruction context.<br>
9797
/// By default, Anchor will prevents duplicate mutable accounts to avoid potential security issues and unintended behavior.<br>
9898
/// The <code>dup</code> constraint explicitly allows this for cases where it's intentional and safe.<br>
99-
/// This constraint only applies to mutable accounts (<code>mut</code>). Readonly accounts naturally allow duplicates without requiring the <code>dup</code> constraint.<br>
99+
/// This constraint only applies to mutable account (`mut`) types that serialize on exit. Other types like <br>
100+
/// `UncheckedAccount`, `Signer`, `SystemAccount`, `AccountLoader`, `Program`, `Interface` and Readonly accounts <br>
101+
/// naturally allow duplicates as they don't serialize data on exit.<br>
100102
/// Example:
101103
/// <pre><code>
102104
/// #[account(mut)]

lang/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ pub trait AccountsExit<'info>: ToAccountMetas + ToAccountInfos<'info> {
222222
}
223223
}
224224

225+
/// Returns the pubkeys of mutable accounts that serialize on exit.
226+
/// Used by the duplicate mutable account validation to check across
227+
/// composite (nested) account struct boundaries.
228+
pub trait DuplicateMutableAccountKeys {
229+
fn duplicate_mutable_account_keys(&self) -> Vec<Pubkey>;
230+
}
231+
225232
/// The close procedure to initiate garabage collection of an account, allowing
226233
/// one to retrieve the rent exemption.
227234
pub trait AccountsClose<'info>: ToAccountInfos<'info> {
@@ -493,9 +500,9 @@ pub mod prelude {
493500
require_keys_neq, require_neq,
494501
solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source,
495502
system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts,
496-
AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Discriminator, Id,
497-
InitSpace, Key, Lamports, Owner, Owners, ProgramData, Result, Space, ToAccountInfo,
498-
ToAccountInfos, ToAccountMetas,
503+
AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Discriminator,
504+
DuplicateMutableAccountKeys, Id, InitSpace, Key, Lamports, Owner, Owners, ProgramData,
505+
Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas,
499506
};
500507
// Re-export the crate as anchor_lang for declare_program! macro
501508
pub use crate as anchor_lang;

0 commit comments

Comments
 (0)