Skip to content

Commit cbc1f7c

Browse files
lang: Fix using non-instruction composite accounts multiple times with declare_program! (solana-foundation#4113)
1 parent c248d9f commit cbc1f7c

File tree

5 files changed

+144
-23
lines changed

5 files changed

+144
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The minor version will be incremented upon a breaking change and the patch versi
1717
### Fixes
1818

1919
- idl: Fix defined types with unsupported fields not producing an error ([#4088](https://github.com/solana-foundation/anchor/pull/4088)).
20+
- lang: Fix using non-instruction composite accounts multiple times with `declare_program!` ([#4113](https://github.com/solana-foundation/anchor/pull/4113)).
2021

2122
### Breaking
2223

lang/attribute/program/src/declare_program/mods/internal.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ fn gen_internal_accounts_common(
109109
) -> proc_macro2::TokenStream {
110110
// It's possible to declare an accounts struct and not use it as an instruction, see
111111
// https://github.com/coral-xyz/anchor/issues/3274
112+
//
113+
// NOTE: Returned accounts will not be unique if non-instruction composite accounts have been
114+
// used multiple times https://github.com/solana-foundation/anchor/issues/3349
112115
fn get_non_instruction_composite_accounts<'a>(
113116
accs: &'a [IdlInstructionAccountItem],
114117
idl: &'a Idl,
@@ -121,35 +124,53 @@ fn gen_internal_accounts_common(
121124
.iter()
122125
.any(|ix| ix.accounts == accs.accounts) =>
123126
{
124-
let mut non_ix_composite_accs =
125-
get_non_instruction_composite_accounts(&accs.accounts, idl);
126-
if !non_ix_composite_accs.contains(&accs) {
127-
non_ix_composite_accs.push(accs);
128-
}
129-
non_ix_composite_accs
127+
let mut nica = get_non_instruction_composite_accounts(&accs.accounts, idl);
128+
nica.push(accs);
129+
nica
130130
}
131131
_ => Default::default(),
132132
})
133133
.collect()
134134
}
135135

136+
// Combine regular instructions with non-instruction composite accounts
136137
let ix_accs = idl
137138
.instructions
138139
.iter()
139140
.flat_map(|ix| ix.accounts.to_owned())
140141
.collect::<Vec<_>>();
141142
let combined_ixs = get_non_instruction_composite_accounts(&ix_accs, idl)
142143
.into_iter()
143-
.map(|accs| IdlInstruction {
144-
// The name is not guaranteed to be the same as the one used in the actual source code
145-
// of the program because the IDL only stores the field names.
146-
name: accs.name.to_owned(),
147-
accounts: accs.accounts.to_owned(),
148-
args: Default::default(),
149-
discriminator: Default::default(),
150-
docs: Default::default(),
151-
returns: Default::default(),
144+
.fold(Vec::<IdlInstruction>::default(), |mut ixs, accs| {
145+
// Make sure they are unique
146+
if ixs.iter().all(|ix| ix.accounts != accs.accounts) {
147+
// The name is not guaranteed to be the same as the one used in the actual source
148+
// code of the program because the IDL only stores the field names.
149+
let name = if ixs.iter().all(|ix| ix.name != accs.name) {
150+
accs.name.to_owned()
151+
} else {
152+
// Append numbers to the field name until we find a unique name
153+
(2..)
154+
.find_map(|i| {
155+
let name = format!("{}{i}", accs.name);
156+
ixs.iter().all(|ix| ix.name != name).then_some(name)
157+
})
158+
.expect("Should always find a valid name")
159+
};
160+
161+
ixs.push(IdlInstruction {
162+
name,
163+
accounts: accs.accounts.to_owned(),
164+
args: Default::default(),
165+
discriminator: Default::default(),
166+
docs: Default::default(),
167+
returns: Default::default(),
168+
})
169+
}
170+
171+
ixs
152172
})
173+
.into_iter()
153174
.chain(idl.instructions.iter().cloned())
154175
.collect::<Vec<_>>();
155176

tests/declare-program/idls/external.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,61 @@
258258
"type": "u32"
259259
}
260260
]
261+
},
262+
{
263+
"name": "update_non_instruction_composite2",
264+
"discriminator": [
265+
218,
266+
106,
267+
203,
268+
167,
269+
112,
270+
177,
271+
145,
272+
22
273+
],
274+
"accounts": [
275+
{
276+
"name": "non_instruction_update",
277+
"accounts": [
278+
{
279+
"name": "program",
280+
"address": "Externa111111111111111111111111111111111111"
281+
}
282+
]
283+
},
284+
{
285+
"name": "non_instruction_update_with_different_ident",
286+
"accounts": [
287+
{
288+
"name": "authority",
289+
"signer": true
290+
},
291+
{
292+
"name": "my_account",
293+
"writable": true,
294+
"pda": {
295+
"seeds": [
296+
{
297+
"kind": "account",
298+
"path": "authority"
299+
}
300+
]
301+
}
302+
},
303+
{
304+
"name": "program",
305+
"address": "Externa111111111111111111111111111111111111"
306+
}
307+
]
308+
}
309+
],
310+
"args": [
311+
{
312+
"name": "value",
313+
"type": "u32"
314+
}
315+
]
261316
}
262317
],
263318
"accounts": [

tests/declare-program/programs/declare-program/src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,26 @@ pub mod declare_program {
6464
},
6565
},
6666
);
67-
external::cpi::update_non_instruction_composite(cpi_ctx, value)?;
67+
external::cpi::update_non_instruction_composite(cpi_ctx, 10)?;
68+
cpi_my_account.reload()?;
69+
require_eq!(cpi_my_account.field, 10);
70+
71+
// Composite accounts but not an actual instruction (intentionally checking multiple times)
72+
let cpi_ctx = CpiContext::new(
73+
ctx.accounts.external_program.key(),
74+
external::cpi::accounts::UpdateNonInstructionComposite2 {
75+
non_instruction_update: external::cpi::accounts::NonInstructionUpdate2 {
76+
program: ctx.accounts.external_program.to_account_info(),
77+
},
78+
non_instruction_update_with_different_ident:
79+
external::cpi::accounts::NonInstructionUpdate {
80+
authority: ctx.accounts.authority.to_account_info(),
81+
my_account: cpi_my_account.to_account_info(),
82+
program: ctx.accounts.external_program.to_account_info(),
83+
},
84+
},
85+
);
86+
external::cpi::update_non_instruction_composite2(cpi_ctx, value)?;
6887
cpi_my_account.reload()?;
6988
require_eq!(cpi_my_account.field, value);
7089

tests/declare-program/programs/external/src/lib.rs

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ pub mod external {
3535
Ok(())
3636
}
3737

38+
// Test the issue described in https://github.com/coral-xyz/anchor/issues/3349
39+
pub fn update_non_instruction_composite2(
40+
ctx: Context<UpdateNonInstructionComposite2>,
41+
value: u32,
42+
) -> Result<()> {
43+
ctx.accounts
44+
.non_instruction_update_with_different_ident
45+
.my_account
46+
.field = value;
47+
Ok(())
48+
}
49+
3850
// Compilation test for whether a defined type (an account in this case) can be used in `cpi` client.
3951
pub fn test_compilation_defined_type_param(
4052
_ctx: Context<TestCompilation>,
@@ -92,6 +104,24 @@ pub struct Update<'info> {
92104
pub my_account: Account<'info, MyAccount>,
93105
}
94106

107+
#[derive(Accounts)]
108+
pub struct UpdateComposite<'info> {
109+
pub update: Update<'info>,
110+
}
111+
112+
#[derive(Accounts)]
113+
pub struct UpdateNonInstructionComposite<'info> {
114+
pub non_instruction_update: NonInstructionUpdate<'info>,
115+
}
116+
117+
#[derive(Accounts)]
118+
pub struct UpdateNonInstructionComposite2<'info> {
119+
// Intenionally using different composite account with the same identifier
120+
// https://github.com/solana-foundation/anchor/pull/3350#pullrequestreview-2425405970
121+
pub non_instruction_update: NonInstructionUpdate2<'info>,
122+
pub non_instruction_update_with_different_ident: NonInstructionUpdate<'info>,
123+
}
124+
95125
#[derive(Accounts)]
96126
pub struct NonInstructionUpdate<'info> {
97127
pub authority: Signer<'info>,
@@ -101,13 +131,8 @@ pub struct NonInstructionUpdate<'info> {
101131
}
102132

103133
#[derive(Accounts)]
104-
pub struct UpdateComposite<'info> {
105-
pub update: Update<'info>,
106-
}
107-
108-
#[derive(Accounts)]
109-
pub struct UpdateNonInstructionComposite<'info> {
110-
pub non_instruction_update: NonInstructionUpdate<'info>,
134+
pub struct NonInstructionUpdate2<'info> {
135+
pub program: Program<'info, program::External>,
111136
}
112137

113138
#[account]

0 commit comments

Comments
 (0)