Skip to content

Commit dcb7b16

Browse files
committed
token-2022 transfer-hook counter: reuse the existing counter when configuring another mint
1 parent 7d56d4f commit dcb7b16

2 files changed

Lines changed: 70 additions & 11 deletions

File tree

‎tokens/token-2022/transfer-hook/counter/pinocchio/program/src/instructions/initialize_extra_account_meta_list.rs‎

Lines changed: 22 additions & 11 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.
@@ -108,18 +111,26 @@ pub fn initialize_extra_account_meta_list(program_id: &Address, accounts: &mut [
108111
return Err(ProgramError::InvalidSeeds);
109112
}
110113

111-
let counter_bump_bytes = [counter_bump];
112-
let counter_seeds = [Seed::from(COUNTER_SEED), Seed::from(&counter_bump_bytes)];
114+
// Because that counter is global rather than per-mint, setting up a second
115+
// mint finds it already there. Creating it again would fail and take the
116+
// whole instruction — including the new mint's list — down with it, so the
117+
// existing account is reused instead.
118+
if counter.is_data_empty() {
119+
let counter_bump_bytes = [counter_bump];
120+
let counter_seeds = [Seed::from(COUNTER_SEED), Seed::from(&counter_bump_bytes)];
113121

114-
log!("Creating counter");
115-
CreateAccount {
116-
from: payer,
117-
to: counter,
118-
lamports: Rent::get()?.try_minimum_balance(COUNTER_SIZE)?,
119-
space: COUNTER_SIZE as u64,
120-
owner: program_id,
122+
log!("Creating counter");
123+
CreateAccount {
124+
from: payer,
125+
to: counter,
126+
lamports: Rent::get()?.try_minimum_balance(COUNTER_SIZE)?,
127+
space: COUNTER_SIZE as u64,
128+
owner: program_id,
129+
}
130+
.invoke_signed(&[Signer::from(&counter_seeds)])?;
131+
} else if !counter.owned_by(program_id) || counter.data_len() != COUNTER_SIZE {
132+
return Err(TransferHookError::InvalidCounterAccount.into());
121133
}
122-
.invoke_signed(&[Signer::from(&counter_seeds)])?;
123134

124135
log!("Extra account meta list created");
125136
Ok(())

‎tokens/token-2022/transfer-hook/counter/pinocchio/tests/test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,54 @@ describe('Token-2022 Transfer Hook — Counter (Pinocchio)', () => {
361361
assert.equal(counterValue(), 2n, 'the counter advanced to two');
362362
});
363363

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

0 commit comments

Comments
 (0)