Skip to content

Commit 48aba30

Browse files
authored
feat: Disallow duplicate *mutable* accounts by default (#3946)
* feat: Add duplicate mutable account constraint * feat: tests for duplicate mutable accounts * feat: add test for duplicate mutable accounts in workflow * style(tests): prettier * chore: update benchmarks * feat: exclude UncheckedAccounts from duplicate mutable account checks * feat(tests): add duplicate-mutable-accounts to test scripts * feat: enhance duplicate mutable account checks for optional fields * chore(bench): update * fix: update program IDs * feat: allow duplicate accounts in realloc2 ix * chore(bench): update * chore(bench): update * feat(tests): allow duplicate accounts in misc tests * fix(bench):update * fix: update program ID for duplicate mutable accounts * fix: update program ID * fix(bench): update * chore(docs): Updated docs and CHANGELOG.md * refactor: ignore init accounts * fix(bench): update * chore: formating * refactor: optimize duplicate mutable checks generation * refactor: replace BTreeSet with HashSet * (chore): Update benchmarks * test(events): use confirmOptions for transaction handling * chore: Update CHANGELOG.md * feat(lang): Added checks for duplicate mutable accounts in `remaining_accounts` to prevent validation bypass. * feat(tests): Add nested duplicate account test to prevent mutable account conflicts * chore(bench): Update benchmarks * fix(lang): Exclude Signer accounts from duplicate mutable checks in account validation * feat(tests): Add test to initialize multiple accounts with the same payer * chore(bench): Update * fix: package.json * refactor: remove unused confirmOptions from event tests * chore: update benchmarks * chore: update benchmarks * Update benchmarks * chore: Update benchmarks
1 parent ea0d744 commit 48aba30

File tree

26 files changed

+829
-126
lines changed

26 files changed

+829
-126
lines changed

.github/workflows/reusable-tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,8 @@ jobs:
418418
path: tests/custom-program
419419
- cmd: cd tests/typescript && anchor test --skip-lint && npx tsc --noEmit
420420
path: tests/typescript
421+
- cmd: cd tests/duplicate-mutable-accounts && anchor test --skip-lint
422+
path: tests/duplicate-mutable-accounts
421423
# zero-copy tests cause `/usr/bin/ld: final link failed: No space left on device`
422424
# on GitHub runners. It is likely caused by `cargo test-sbf` since all other tests
423425
# don't have this problem.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ The minor version will be incremented upon a breaking change and the patch versi
2121

2222
### Breaking
2323

24+
- lang: Disallow duplicate mutable accounts by default. But allows duplicate mutable accounts in instruction contexts using `dup` constraint ([#3946](https://github.com/solana-foundation/anchor/pull/3946)).
25+
2426
## [0.32.1] - 2025-10-09
2527

2628
### Features

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 | 932,424 | 🟢 **-194,416 (17.25%)** |
19+
| Program | Binary Size | - |
20+
| ------- | ----------- | ----------------------- |
21+
| bench | 1,015,624 | 🟢 **-111,216 (9.87%)** |
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 | 702 | 🔴 **+17 (2.48%)** |
22-
| accountInfo2 | 1,124 | 🔴 **+71 (6.74%)** |
23-
| accountInfo4 | 1,921 | 🔴 **+171 (9.77%)** |
24-
| accountInfo8 | 3,480 | 🔴 **+345 (11.00%)** |
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%)** |
2525
| accountEmptyInit1 | 4,770 | 🟢 **-145 (2.95%)** |
26-
| accountEmpty1 | 738 | 🟢 **-36 (4.65%)** |
26+
| accountEmpty1 | 773 | 🟢 **-1 (0.13%)** |
2727
| accountEmptyInit2 | 8,487 | 🟢 **-306 (3.48%)** |
28-
| accountEmpty2 | 1,138 | 🟢 **-36 (3.07%)** |
28+
| accountEmpty2 | 1,173 | 🟢 **-1 (0.09%)** |
2929
| accountEmptyInit4 | 15,915 | 🟢 **-833 (4.97%)** |
30-
| accountEmpty4 | 1,923 | 🟢 **-39 (1.99%)** |
30+
| accountEmpty4 | 1,960 | 🟢 **-2 (0.10%)** |
3131
| accountEmptyInit8 | 30,779 | 🟢 **-1,578 (4.88%)** |
32-
| accountEmpty8 | 3,500 | 🟢 **-48 (1.35%)** |
32+
| accountEmpty8 | 3,539 | 🟢 **-9 (0.25%)** |
3333
| accountSizedInit1 | 4,864 | 🟢 **-155 (3.09%)** |
34-
| accountSized1 | 786 | 🟢 **-36 (4.38%)** |
34+
| accountSized1 | 820 | 🟢 **-2 (0.24%)** |
3535
| accountSizedInit2 | 8,654 | 🟢 **-327 (3.64%)** |
36-
| accountSized2 | 1,198 | 🟢 **-42 (3.39%)** |
36+
| accountSized2 | 1,236 | 🟢 **-4 (0.32%)** |
3737
| accountSizedInit4 | 16,236 | 🟢 **-918 (5.35%)** |
38-
| accountSized4 | 2,031 | 🟢 **-51 (2.45%)** |
38+
| accountSized4 | 2,070 | 🟢 **-12 (0.58%)** |
3939
| accountSizedInit8 | 31,363 | 🟢 **-1,650 (5.00%)** |
40-
| accountSized8 | 3,694 | 🟢 **-68 (1.81%)** |
40+
| accountSized8 | 3,731 | 🟢 **-31 (0.82%)** |
4141
| accountUnsizedInit1 | 4,967 | 🟢 **-160 (3.12%)** |
42-
| accountUnsized1 | 814 | 🟢 **-60 (6.86%)** |
42+
| accountUnsized1 | 854 | 🟢 **-20 (2.29%)** |
4343
| accountUnsizedInit2 | 8,841 | 🟢 **-410 (4.43%)** |
44-
| accountUnsized2 | 1,240 | 🟢 **-86 (6.49%)** |
44+
| accountUnsized2 | 1,281 | 🟢 **-45 (3.39%)** |
4545
| accountUnsizedInit4 | 16,559 | 🟢 **-819 (4.71%)** |
46-
| accountUnsized4 | 2,093 | 🟢 **-138 (6.19%)** |
46+
| accountUnsized4 | 2,134 | 🟢 **-97 (4.35%)** |
4747
| accountUnsizedInit8 | 31,985 | 🟢 **-1,976 (5.82%)** |
48-
| accountUnsized8 | 3,797 | 🟢 **-238 (5.90%)** |
48+
| accountUnsized8 | 3,837 | 🟢 **-198 (4.91%)** |
4949
| boxedAccountEmptyInit1 | 4,864 | 🟢 **-143 (2.86%)** |
50-
| boxedAccountEmpty1 | 831 | 🟢 **-33 (3.82%)** |
50+
| boxedAccountEmpty1 | 864 | - |
5151
| boxedAccountEmptyInit2 | 8,604 | 🟢 **-302 (3.39%)** |
52-
| boxedAccountEmpty2 | 1,253 | 🟢 **-33 (2.57%)** |
52+
| boxedAccountEmpty2 | 1,285 | 🟢 **-1 (0.08%)** |
5353
| boxedAccountEmptyInit4 | 16,075 | 🟢 **-827 (4.89%)** |
54-
| boxedAccountEmpty4 | 2,077 | 🟢 **-38 (1.80%)** |
54+
| boxedAccountEmpty4 | 2,112 | 🟢 **-3 (0.14%)** |
5555
| boxedAccountEmptyInit8 | 31,026 | 🟢 **-1,565 (4.80%)** |
5656
| boxedAccountEmpty8 | 3,797 | 🟢 **-4 (0.11%)** |
5757
| boxedAccountSizedInit1 | 4,952 | 🟢 **-151 (2.96%)** |
58-
| boxedAccountSized1 | 877 | 🟢 **-35 (3.84%)** |
58+
| boxedAccountSized1 | 913 | 🔴 **+1 (0.11%)** |
5959
| boxedAccountSizedInit2 | 8,756 | 🟢 **-319 (3.52%)** |
60-
| boxedAccountSized2 | 1,318 | 🟢 **-37 (2.73%)** |
60+
| boxedAccountSized2 | 1,351 | 🟢 **-4 (0.30%)** |
6161
| boxedAccountSizedInit4 | 16,355 | 🟢 **-859 (4.99%)** |
62-
| boxedAccountSized4 | 2,185 | 🟢 **-46 (2.06%)** |
62+
| boxedAccountSized4 | 2,218 | 🟢 **-13 (0.58%)** |
6363
| boxedAccountSizedInit8 | 31,562 | 🟢 **-1,959 (5.84%)** |
6464
| boxedAccountSized8 | 3,984 | 🟢 **-23 (0.57%)** |
6565
| boxedAccountUnsizedInit1 | 5,044 | 🟢 **-158 (3.04%)** |
66-
| boxedAccountUnsized1 | 907 | 🟢 **-57 (5.91%)** |
66+
| boxedAccountUnsized1 | 943 | 🟢 **-21 (2.18%)** |
6767
| boxedAccountUnsizedInit2 | 8,916 | 🟢 **-335 (3.62%)** |
68-
| boxedAccountUnsized2 | 1,352 | 🟢 **-82 (5.72%)** |
68+
| boxedAccountUnsized2 | 1,386 | 🟢 **-48 (3.35%)** |
6969
| boxedAccountUnsizedInit4 | 16,651 | 🟢 **-891 (5.08%)** |
70-
| boxedAccountUnsized4 | 2,234 | 🟢 **-133 (5.62%)** |
70+
| boxedAccountUnsized4 | 2,270 | 🟢 **-97 (4.10%)** |
7171
| boxedAccountUnsizedInit8 | 32,130 | 🟢 **-2,023 (5.92%)** |
7272
| boxedAccountUnsized8 | 4,063 | 🟢 **-194 (4.56%)** |
73-
| boxedInterfaceAccountMint1 | 1,092 | 🟢 **-18 (1.62%)** |
74-
| boxedInterfaceAccountMint2 | 1,490 | 🟢 **-44 (2.87%)** |
75-
| boxedInterfaceAccountMint4 | 2,276 | 🟢 **-94 (3.97%)** |
73+
| boxedInterfaceAccountMint1 | 1,128 | 🔴 **+18 (1.62%)** |
74+
| boxedInterfaceAccountMint2 | 1,523 | 🟢 **-11 (0.72%)** |
75+
| boxedInterfaceAccountMint4 | 2,307 | 🟢 **-63 (2.66%)** |
7676
| boxedInterfaceAccountMint8 | 3,907 | 🟢 **-157 (3.86%)** |
77-
| boxedInterfaceAccountToken1 | 1,219 | 🟢 **-27 (2.17%)** |
78-
| boxedInterfaceAccountToken2 | 1,732 | 🟢 **-62 (3.46%)** |
79-
| boxedInterfaceAccountToken4 | 2,748 | 🟢 **-130 (4.52%)** |
77+
| boxedInterfaceAccountToken1 | 1,255 | 🔴 **+9 (0.72%)** |
78+
| boxedInterfaceAccountToken2 | 1,765 | 🟢 **-29 (1.62%)** |
79+
| boxedInterfaceAccountToken4 | 2,779 | 🟢 **-99 (3.44%)** |
8080
| boxedInterfaceAccountToken8 | 4,839 | 🟢 **-229 (4.52%)** |
81-
| interfaceAccountMint1 | 1,107 | 🟢 **-19 (1.69%)** |
82-
| interfaceAccountMint2 | 1,494 | 🟢 **-68 (4.35%)** |
83-
| interfaceAccountMint4 | 2,276 | 🟢 **-156 (6.41%)** |
81+
| interfaceAccountMint1 | 1,145 | 🔴 **+19 (1.69%)** |
82+
| interfaceAccountMint2 | 1,533 | 🟢 **-29 (1.86%)** |
83+
| interfaceAccountMint4 | 2,313 | 🟢 **-119 (4.89%)** |
8484
| interfaceAccountMint8 | 3,835 | 🟢 **-328 (7.88%)** |
85-
| interfaceAccountToken1 | 1,239 | 🟢 **-29 (2.29%)** |
86-
| interfaceAccountToken2 | 1,753 | 🟢 **-96 (5.19%)** |
87-
| interfaceAccountToken4 | 2,783 | 🟢 **-214 (7.14%)** |
88-
| interface1 | 883 | 🔴 **+5 (0.57%)** |
89-
| interface2 | 1,029 | 🔴 **+6 (0.59%)** |
90-
| interface4 | 1,308 | 🔴 **+7 (0.54%)** |
91-
| interface8 | 1,887 | 🔴 **+20 (1.07%)** |
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,244 | 🔴 **+71 (6.05%)** |
98-
| signer4 | 1,904 | 🔴 **+145 (8.24%)** |
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,233 | 🔴 **+71 (6.11%)** |
106-
| uncheckedAccount4 | 1,861 | 🔴 **+145 (8.45%)** |
107-
| uncheckedAccount8 | 3,171 | 🔴 **+338 (11.93%)** |
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%)** |
108108

109109
### Notable changes
110110

bench/STACK_MEMORY.md

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

1717
Solana version: 2.3.0
1818

19-
| Instruction | Stack Memory | - |
20-
| ----------- | ------------ | --- |
19+
| Instruction | Stack Memory | - |
20+
| ------------------------------ | ------------ | ------------------- |
21+
| account_info1 | 46 | - |
22+
| account_info2 | 88 | - |
23+
| account_info4 | 88 | - |
24+
| account_info8 | 88 | - |
25+
| account_empty_init1 | 88 | - |
26+
| account_empty_init2 | 88 | - |
27+
| account_empty_init4 | 88 | - |
28+
| account_empty_init8 | 88 | - |
29+
| account_empty1 | 88 | - |
30+
| account_empty2 | 88 | - |
31+
| account_empty4 | 88 | - |
32+
| account_empty8 | 88 | - |
33+
| account_sized_init1 | 88 | - |
34+
| account_sized_init2 | 88 | - |
35+
| account_sized_init4 | 88 | - |
36+
| account_sized_init8 | 88 | - |
37+
| account_sized1 | 88 | - |
38+
| account_sized2 | 88 | - |
39+
| account_sized4 | 88 | - |
40+
| account_sized8 | 88 | - |
41+
| account_unsized_init1 | 88 | - |
42+
| account_unsized_init2 | 88 | - |
43+
| account_unsized_init4 | 88 | - |
44+
| account_unsized_init8 | 88 | - |
45+
| account_unsized1 | 88 | - |
46+
| account_unsized2 | 88 | - |
47+
| account_unsized4 | 88 | - |
48+
| account_unsized8 | 88 | - |
49+
| boxed_account_empty_init1 | 88 | - |
50+
| boxed_account_empty_init2 | 88 | - |
51+
| boxed_account_empty_init4 | 88 | - |
52+
| boxed_account_empty_init8 | 88 | - |
53+
| boxed_account_empty1 | 88 | - |
54+
| boxed_account_empty2 | 88 | - |
55+
| boxed_account_empty4 | 88 | - |
56+
| boxed_account_empty8 | 96 | 🔴 **+8 (9.09%)** |
57+
| boxed_account_sized_init1 | 88 | - |
58+
| boxed_account_sized_init2 | 88 | - |
59+
| boxed_account_sized_init4 | 88 | - |
60+
| boxed_account_sized_init8 | 88 | - |
61+
| boxed_account_sized1 | 88 | - |
62+
| boxed_account_sized2 | 88 | - |
63+
| boxed_account_sized4 | 88 | - |
64+
| boxed_account_sized8 | 96 | 🔴 **+8 (9.09%)** |
65+
| boxed_account_unsized_init1 | 88 | - |
66+
| boxed_account_unsized_init2 | 88 | - |
67+
| boxed_account_unsized_init4 | 88 | - |
68+
| boxed_account_unsized_init8 | 88 | - |
69+
| boxed_account_unsized1 | 88 | - |
70+
| boxed_account_unsized2 | 88 | - |
71+
| boxed_account_unsized4 | 88 | - |
72+
| boxed_account_unsized8 | 96 | 🔴 **+8 (9.09%)** |
73+
| boxed_interface_account_mint1 | 88 | - |
74+
| boxed_interface_account_mint2 | 88 | - |
75+
| boxed_interface_account_mint4 | 88 | - |
76+
| boxed_interface_account_mint8 | 96 | 🔴 **+8 (9.09%)** |
77+
| boxed_interface_account_token1 | 88 | - |
78+
| boxed_interface_account_token2 | 88 | - |
79+
| boxed_interface_account_token4 | 88 | - |
80+
| boxed_interface_account_token8 | 96 | 🔴 **+8 (9.09%)** |
81+
| interface_account_mint1 | 88 | - |
82+
| interface_account_mint2 | 88 | - |
83+
| interface_account_mint4 | 88 | - |
84+
| interface_account_mint8 | 88 | - |
85+
| interface_account_token1 | 104 | 🔴 **+24 (30.00%)** |
86+
| interface_account_token2 | 104 | 🔴 **+24 (30.00%)** |
87+
| interface_account_token4 | 104 | 🔴 **+24 (30.00%)** |
88+
| interface1 | 88 | - |
89+
| interface2 | 88 | - |
90+
| interface4 | 88 | - |
91+
| interface8 | 88 | - |
92+
| program1 | 88 | - |
93+
| program2 | 88 | - |
94+
| program4 | 88 | - |
95+
| program8 | 88 | - |
96+
| signer1 | 88 | - |
97+
| signer2 | 88 | - |
98+
| signer4 | 88 | - |
99+
| signer8 | 88 | - |
100+
| system_account1 | 88 | - |
101+
| system_account2 | 88 | - |
102+
| system_account4 | 88 | - |
103+
| system_account8 | 88 | - |
104+
| unchecked_account1 | 88 | - |
105+
| unchecked_account2 | 88 | - |
106+
| unchecked_account4 | 88 | - |
107+
| unchecked_account8 | 88 | - |
21108

22109
### Notable changes
23110

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,33 @@ Examples: [Github](https://github.com/solana-developers/anchor-examples/tree/mai
3838
#[account(mut @ <custom_error>)]
3939
```
4040

41+
### `#[account(dup)]`
42+
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.
45+
46+
**Note**: This constraint only applies to mutable accounts (`mut`). Readonly accounts naturally allow duplicates without requiring the `dup` constraint.
47+
48+
```rust title="attribute"
49+
#[account(mut, dup)]
50+
#[account(mut, dup @ <custom_error>)]
51+
```
52+
53+
```rust title="snippet"
54+
#[derive(Accounts)]
55+
pub struct AllowsDuplicateMutable<'info> {
56+
#[account(mut)]
57+
pub account1: Account<'info, Counter>,
58+
// This account can be the same as account1
59+
#[account(mut, dup)]
60+
pub account2: Account<'info, Counter>,
61+
}
62+
63+
pub fn allows_duplicate_mutable(ctx: Context<AllowsDuplicateMutable>) -> Result<()> {
64+
Ok(())
65+
}
66+
```
67+
4168
### `#[account(init)]`
4269

4370
Description: Creates the account via a CPI to the system program and initializes

0 commit comments

Comments
 (0)