Skip to content

Commit 2d6e515

Browse files
committed
token-2022 transfer-hook account-data-as-seed: reuse the existing counter when configuring another mint
1 parent 83cedb5 commit 2d6e515

2 files changed

Lines changed: 72 additions & 12 deletions

File tree

tokens/token-2022/transfer-hook/account-data-as-seed/pinocchio/program/src/instructions/initialize_extra_account_meta_list.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use pinocchio::{
77
use pinocchio_log::log;
88
use pinocchio_system::instructions::CreateAccount;
99

10-
use crate::instructions::{COUNTER_SEED, COUNTER_SIZE, EXTRA_ACCOUNT_METAS_SEED};
10+
use crate::{
11+
error::TransferHookError,
12+
instructions::{COUNTER_SEED, COUNTER_SIZE, EXTRA_ACCOUNT_METAS_SEED},
13+
};
1114

1215
/// A serialized `ExtraAccountMetaList` holding this example's one extra
1316
/// account: the counter PDA.
@@ -121,19 +124,27 @@ pub fn initialize_extra_account_meta_list(program_id: &Address, accounts: &mut [
121124
return Err(ProgramError::InvalidSeeds);
122125
}
123126

124-
let counter_bump_bytes = [counter_bump];
125-
let counter_seeds =
126-
[Seed::from(COUNTER_SEED), Seed::from(payer.address().as_ref()), Seed::from(&counter_bump_bytes)];
127+
// The counter is per owner, not per mint, so configuring a second mint
128+
// finds the payer's counter already there. Creating it again would fail and
129+
// take the whole instruction — including the new mint's list — down with
130+
// it, so the existing account is reused instead.
131+
if counter.is_data_empty() {
132+
let counter_bump_bytes = [counter_bump];
133+
let counter_seeds =
134+
[Seed::from(COUNTER_SEED), Seed::from(payer.address().as_ref()), Seed::from(&counter_bump_bytes)];
127135

128-
log!("Creating counter");
129-
CreateAccount {
130-
from: payer,
131-
to: counter,
132-
lamports: Rent::get()?.try_minimum_balance(COUNTER_SIZE)?,
133-
space: COUNTER_SIZE as u64,
134-
owner: program_id,
136+
log!("Creating counter");
137+
CreateAccount {
138+
from: payer,
139+
to: counter,
140+
lamports: Rent::get()?.try_minimum_balance(COUNTER_SIZE)?,
141+
space: COUNTER_SIZE as u64,
142+
owner: program_id,
143+
}
144+
.invoke_signed(&[Signer::from(&counter_seeds)])?;
145+
} else if !counter.owned_by(program_id) || counter.data_len() != COUNTER_SIZE {
146+
return Err(TransferHookError::InvalidCounterAccount.into());
135147
}
136-
.invoke_signed(&[Signer::from(&counter_seeds)])?;
137148

138149
log!("Extra account meta list created");
139150
Ok(())

tokens/token-2022/transfer-hook/account-data-as-seed/pinocchio/tests/test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,55 @@ describe('Token-2022 Transfer Hook — Account Data As Seed (Pinocchio)', () =>
366366
assert.equal(counterValue(), 2n, 'the counter advanced to two');
367367
});
368368

369+
it('Configures a second mint against the existing counter', async () => {
370+
// The counter is keyed by owner, not by mint, so setting up a second
371+
// mint for the same payer must reuse it. Creating it again would fail
372+
// and roll the whole setup back, leaving every mint after the first
373+
// unable to use this hook.
374+
const secondMint = await generateKeyPairSigner();
375+
const initIx = {
376+
programAddress: programId,
377+
accounts: [
378+
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
379+
{ address: secondMint.address, role: AccountRole.WRITABLE_SIGNER, signer: secondMint },
380+
{ address: TOKEN_2022_PROGRAM_ADDRESS, role: AccountRole.READONLY },
381+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
382+
],
383+
data: Uint8Array.of(INITIALIZE_DISCRIMINATOR, DECIMALS),
384+
};
385+
send(await tx([initIx]), 'initialize second mint');
386+
387+
const [secondMetaList] = await getProgramDerivedAddress({
388+
programAddress: programId,
389+
seeds: ['extra-account-metas', addressEncoder.encode(secondMint.address)],
390+
});
391+
const countBefore = counterValue();
392+
393+
const metasIx = {
394+
programAddress: programId,
395+
accounts: [
396+
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
397+
{ address: secondMetaList, role: AccountRole.WRITABLE },
398+
{ address: secondMint.address, role: AccountRole.READONLY },
399+
{ address: counter, role: AccountRole.WRITABLE },
400+
{ address: TOKEN_2022_PROGRAM_ADDRESS, role: AccountRole.READONLY },
401+
{ address: ASSOCIATED_TOKEN_PROGRAM_ADDRESS, role: AccountRole.READONLY },
402+
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
403+
],
404+
data: INITIALIZE_EXTRA_ACCOUNT_META_LIST_DISCRIMINATOR,
405+
};
406+
send(await tx([metasIx]), 'initialize second extra account meta list');
407+
408+
const account = svm.getAccount(secondMetaList);
409+
if (!account?.exists) throw new Error('second extra account meta list not found');
410+
assert.deepEqual(
411+
Array.from(account.data),
412+
Array.from(EXPECTED_EXTRA_ACCOUNT_METAS),
413+
'the second mint got its own list',
414+
);
415+
assert.equal(counterValue(), countBefore, "the owner's counter kept its value");
416+
});
417+
369418
it('Rejects calling the hook outside a transfer', async () => {
370419
// Same accounts Token-2022 would pass, but invoked directly. The source
371420
// account's `transferring` flag is only set mid-transfer, so this fails.

0 commit comments

Comments
 (0)