Skip to content

add pinocchio token-2022 mint-close-authority example#624

Open
MarkFeder wants to merge 4 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-2022-mint-close-authority-pinocchio
Open

add pinocchio token-2022 mint-close-authority example#624
MarkFeder wants to merge 4 commits into
solana-foundation:mainfrom
MarkFeder:tokens-token-2022-mint-close-authority-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

Adds a Pinocchio port of the tokens/token-2022/mint-close-authority example, alongside the existing anchor and native versions.

What it does

A single instruction creates an SPL Token-2022 mint carrying the MintCloseAuthority extension, so a designated authority can later close the mint and reclaim its rent.

Notes

Token-2022 has no Pinocchio wrapper crate (pinocchio-token targets the legacy SPL Token program), so the Token-2022 instructions are built by hand and CPI'd:

  1. CreateAccount for the mint, sized to 202 bytes (base Account length 165 + account-type byte + one MintCloseAuthority TLV entry) and owned by the Token-2022 program.
  2. InitializeMintCloseAuthority (variant 25) — must run before the mint is initialized.
  3. InitializeMint (variant 0).

The bankrun test asserts the resulting mint is owned by Token-2022, is exactly 202 bytes, has the correct decimals, and that the extension header (account type Mint, TLV type MintCloseAuthority) and stored close authority were written correctly.

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio port of the tokens/token-2022/mint-close-authority example, placing it alongside the existing anchor and native implementations. Because no pinocchio wrapper exists for Token-2022, the three required instructions — CreateAccount, InitializeMintCloseAuthority (variant 25), and InitializeMint (variant 0) — are built by hand and CPI'd, with TOKEN_2022_PROGRAM_ID hardcoded in each InstructionView rather than read from a caller-supplied account.

  • Instruction encoding: both helper functions (build_initialize_close_authority_data at 34 bytes, build_initialize_mint_data at 67 bytes) correctly use a 1-byte COption tag format matching the Token-2022 pack layout, and the CPI ordering (close-authority extension before InitializeMint) is correct per the Token-2022 requirement.
  • MINT_SIZE (202 bytes): the layout arithmetic (165 base + 1 account-type byte + 2-byte TLV type + 2-byte TLV length + 32-byte value) is accurate and consistently used across the program and test assertions.
  • Test coverage: the bankrun test uses a distinct closeAuthority keypair (separate from payer/mint-authority) and explicitly validates account ownership, size, decimals, TLV type, TLV length, and the stored close-authority address.

Confidence Score: 5/5

Safe to merge; the new pinocchio example is self-contained and adds no risk to existing programs.

The instruction byte encoding, CPI ordering, account layout constants, and test assertions are all correct. The only noteworthy design choice — computing rent with (ACCOUNT_STORAGE_OVERHEAD + MINT_SIZE) * DEFAULT_LAMPORTS_PER_BYTE instead of calling Rent::get() — is explicitly documented in a comment explaining that the constant already folds in the 2-year exemption threshold, consistent with how pinocchio exposes that value.

No files require special attention.

Important Files Changed

Filename Overview
tokens/token-2022/mint-close-authority/pinocchio/program/src/instructions/create_mint.rs Core instruction logic: correct 3-step CPI sequence (CreateAccount → InitializeMintCloseAuthority → InitializeMint) with hardcoded TOKEN_2022_PROGRAM_ID and accurate instruction byte encoding.
tokens/token-2022/mint-close-authority/pinocchio/program/src/instructions/mod.rs Defines TOKEN_2022_PROGRAM_ID, MINT_SIZE (202 bytes), and CreateTokenArgs; layout arithmetic and doc comment are correct (165 base + 1 account-type + 4 TLV header + 32 value = 202).
tokens/token-2022/mint-close-authority/pinocchio/tests/test.ts Bankrun test with distinct closeAuthority keypair; explicitly asserts ownership, account size, decimals, TLV type/length, and stored close-authority address.
tokens/token-2022/mint-close-authority/pinocchio/program/src/processor.rs Single-instruction dispatcher; correctly routes all instruction data to create_mint without a discriminator prefix, matching the native example's wire format.
tokens/token-2022/mint-close-authority/pinocchio/program/Cargo.toml Standard pinocchio crate manifest; dependencies (pinocchio, pinocchio-log, pinocchio-system) are workspace-pinned and appropriate for the no-std program.
tokens/token-2022/mint-close-authority/pinocchio/program/src/lib.rs Minimal no_std entrypoint wiring pinocchio's entrypoint! and nostd_panic_handler! macros to process_instruction.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant OurProgram as token-2022-mint-close-authority (Pinocchio)
    participant SystemProgram as System Program
    participant Token2022 as Token-2022 Program

    Client->>OurProgram: create_mint(decimals, accounts[7])
    OurProgram->>SystemProgram: "CreateAccount(payer to mint, 202 bytes, owner=Token2022)"
    SystemProgram-->>OurProgram: Ok
    OurProgram->>Token2022: InitializeMintCloseAuthority(variant 25)
    Token2022-->>OurProgram: Ok
    OurProgram->>Token2022: InitializeMint(variant 0)
    Token2022-->>OurProgram: Ok
    OurProgram-->>Client: Ok
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant OurProgram as token-2022-mint-close-authority (Pinocchio)
    participant SystemProgram as System Program
    participant Token2022 as Token-2022 Program

    Client->>OurProgram: create_mint(decimals, accounts[7])
    OurProgram->>SystemProgram: "CreateAccount(payer to mint, 202 bytes, owner=Token2022)"
    SystemProgram-->>OurProgram: Ok
    OurProgram->>Token2022: InitializeMintCloseAuthority(variant 25)
    Token2022-->>OurProgram: Ok
    OurProgram->>Token2022: InitializeMint(variant 0)
    Token2022-->>OurProgram: Ok
    OurProgram-->>Client: Ok
Loading

Reviews (7): Last reviewed commit: "token-2022 mint-close-authority pinocchi..." | Re-trigger Greptile

Comment on lines +43 to +90
it("Creates a Token-2022 mint with a close authority", async () => {
const decimals = 9;
const mintKeypair = Keypair.generate();

const data = Buffer.from(borsh.serialize(CreateTokenArgsSchema, { token_decimals: decimals }));

const ix = new TransactionInstruction({
programId: PROGRAM_ID,
keys: [
{ pubkey: mintKeypair.publicKey, isSigner: true, isWritable: true }, // mint account
{ pubkey: payer.publicKey, isSigner: false, isWritable: false }, // mint authority
{ pubkey: payer.publicKey, isSigner: false, isWritable: false }, // close authority
{ pubkey: payer.publicKey, isSigner: true, isWritable: true }, // payer
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, // rent sysvar
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, // system program
{ pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, // Token-2022 program
],
data,
});

const tx = new Transaction();
tx.feePayer = payer.publicKey;
tx.recentBlockhash = context.lastBlockhash;
tx.add(ix);
tx.sign(payer, mintKeypair);
await client.processTransaction(tx);

const mintAccount = await client.getAccount(mintKeypair.publicKey);
if (mintAccount === null) throw new Error("Mint account not found");
const mintData = Buffer.from(mintAccount.data);

// Owned by Token-2022, and sized for exactly one extension.
assert.deepEqual(mintAccount.owner.toBytes(), TOKEN_2022_PROGRAM_ID.toBytes());
assert.equal(mintData.length, EXTENDED_MINT_SIZE);

// Base mint fields were initialized.
assert.equal(mintData[DECIMALS_OFFSET], decimals);

// The extension header marks this as a Mint carrying MintCloseAuthority.
assert.equal(mintData[ACCOUNT_TYPE_OFFSET], ACCOUNT_TYPE_MINT);
assert.equal(mintData.readUInt16LE(TLV_TYPE_OFFSET), MINT_CLOSE_AUTHORITY_EXTENSION);

// The configured close authority was stored in the extension.
const storedCloseAuthority = mintData.subarray(TLV_VALUE_OFFSET, TLV_VALUE_OFFSET + 32);
assert.deepEqual(new Uint8Array(storedCloseAuthority), payer.publicKey.toBytes());

console.log("Mint address:", mintKeypair.publicKey.toBase58());
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Single test case conflates three distinct roles

The test uses payer.publicKey for the mint authority, close authority, and payer simultaneously. This means the test cannot verify that the close authority stored in the TLV is correctly sourced from account index 2 (close authority) rather than index 1 (mint authority) or the payer — all three are identical addresses. A bug that swapped accounts 1 and 2 would go undetected. Using a distinct keypair for at least the close authority would make the assertion at line 87 meaningful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 1b2f802. The test now generates a distinct closeAuthority keypair for account index 2, so the stored-authority assertion genuinely verifies the close authority is sourced from index 2 rather than the mint authority/payer (which remain payer).

Comment on lines +28 to +31
const TLV_TYPE_OFFSET = 166; // u16 LE, 3 == MintCloseAuthority
const TLV_VALUE_OFFSET = 170; // 32-byte close authority pubkey
const DECIMALS_OFFSET = 44; // in the base mint layout
const MINT_CLOSE_AUTHORITY_EXTENSION = 3;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 TLV length field is asserted indirectly, not explicitly

The test validates TLV_TYPE_OFFSET (type = 3) and TLV_VALUE_OFFSET (close authority bytes), but never reads the 2-byte TLV length at offset 168 (TLV_TYPE_OFFSET + 2). Adding assert.equal(mintData.readUInt16LE(168), 32) would fully exercise the on-chain TLV header and catch any future change where Token-2022 alters the MintCloseAuthority value size.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — added assert.equal(mintData.readUInt16LE(168), 32) (TLV_LENGTH_OFFSET) in 1b2f802 so the 2-byte TLV length header is exercised explicitly.

Comment on lines +37 to +41
let [mint_account, mint_authority, close_authority, payer, rent_sysvar, _system_program, _token_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Token-2022 program ID is never validated

_token_program (account index 6) is accepted and silently forwarded to all three CPIs as the callee program, but its address is never compared against TOKEN_2022_PROGRAM_ID. A caller that passes a spoofed program at index 6 would cause the CPIs to target an arbitrary program. For an educational example this is a known trade-off, but adding even a one-line equality check would demonstrate the secure pattern to learners.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The premise here isn't quite accurate: _token_program (index 6) is not forwarded to the CPIs as the callee. Each InstructionView sets program_id: &TOKEN_2022_PROGRAM_ID — the hard-coded constant — and the account isn't even passed in the invoke infos slices (&[mint_account] / &[mint_account, rent_sysvar]). So a spoofed account at index 6 has no effect; the CPIs always target the real Token-2022 program by construction. Index 6 is present only to mirror the native example's account layout. No equality check is needed here since dispatch is already pinned to the constant.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're completely right — I was wrong. Both InstructionView structs hardcode program_id: &TOKEN_2022_PROGRAM_ID, and _token_program doesn't appear in either invoke accounts slice. A spoofed account at index 6 is entirely inert. Apologies for the noise.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@MarkFeder

Copy link
Copy Markdown
Contributor Author

Hi @Perelyn-sama — thanks again for merging my previous pinocchio examples! This one adds the token-2022/mint-close-authority pinocchio port (the first Token-2022 example in the pinocchio series). All checks are green except a single red X on the ASM build-and-test-group-0 — that's the pre-existing repo-wide ASM failure, unrelated to this diff, which #625 fixes. Greptile has it at 5/5 "safe to merge." Would appreciate a review whenever you get a chance. Thanks!

@MarkFeder
MarkFeder force-pushed the tokens-token-2022-mint-close-authority-pinocchio branch from 1b2f802 to 6a49f60 Compare July 8, 2026 21:13
MarkFeder and others added 2 commits July 9, 2026 09:32
Use a distinct close-authority keypair so the stored-authority check
verifies sourcing from account index 2, and assert the TLV length field
(32) explicitly.
@MarkFeder
MarkFeder force-pushed the tokens-token-2022-mint-close-authority-pinocchio branch from 6a49f60 to b40292f Compare July 9, 2026 07:32
@MarkFeder

Copy link
Copy Markdown
Contributor Author

@Perelyn-sama @dev-jodee — rebased onto latest main (picks up the ASM sbpf/Solana pin from #625), CI is now fully green. Ready for review whenever you have a chance 🙏

… run

Move the async bankrun setup out of the `describe` callback and into a
`before` hook so Mocha collects the `it` block (an async `describe` body
registers tests after the suite is already collected, so nothing ran).

With the test now executing, replace `Rent::try_minimum_balance` with the
integer rent formula: its floating-point exemption-threshold path emits an
opcode the bankrun VM rejects ("unsupported BPF instruction"). Matches the
create-token example.
@MarkFeder
MarkFeder force-pushed the tokens-token-2022-mint-close-authority-pinocchio branch from 3f941a9 to 87b819a Compare July 15, 2026 21:52
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Mark, thanks for the work, realized (a bit too late for the already merged project) that you're using web3js, this isn't the proper way to do it anymore, I'd encourage you to look at kit (https://github.com/anza-xyz/kit) and if you could update all the other examples as well, make sure we use the latest standards ! (Would be great if you could do it for the already merged pr from yesterday as well)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dev-jodee — done for this one (59d9542). The test now builds everything with @solana/kit: addresses, the instruction, the transaction message, and signing (generateKeyPairSigner/createKeyPairSignerFromBytes, createTransactionMessage + pipe, signTransactionMessageWithSigners, getTransactionEncoder). Bumped the example's tsconfig target/lib to es2020 for kit's BigInt usage; still 1 passing under bankrun.

One thing worth flagging before I roll this out to the rest: solana-bankrun@0.3.1's BanksClient is still typed against web3.js — start() and getAccount() take a PublicKey, and processTransaction() takes a VersionedTransaction — and it exposes no byte-level entrypoint. So a thin, unavoidable web3.js interop shim remains around bankrun itself: kit builds and signs the tx, then I hand its wire bytes to bankrun via VersionedTransaction.deserialize. Everything the example teaches is kit; web3.js only survives as the harness adapter.

Happy to apply this same pattern across the other pinocchio examples + the merged create-token. If you'd rather also move these off bankrun (e.g. to litesvm) so web3.js drops entirely, say the word and I'll factor that in instead.

Build the transaction with @solana/kit (addresses, instruction, message,
signing) instead of @solana/web3.js. A thin web3.js shim remains only where
solana-bankrun@0.3.1's BanksClient still requires it (start()/getAccount()
PublicKey, processTransaction() VersionedTransaction). Bump tsconfig
target/lib to es2020 for kit's BigInt usage.
@MarkFeder
MarkFeder requested a review from dev-jodee July 19, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants