add pinocchio token-2022 mint-close-authority example#624
Conversation
Greptile SummaryAdds a Pinocchio port of the
Confidence Score: 4/5Safe to merge; the hand-rolled CPI logic is correct and the bankrun test exercises the full happy path end-to-end. The instruction encoding, account layout, and MINT_SIZE arithmetic are all correct. The test is functional but uses a single key for all three roles, which leaves a class of account-swap bugs untestable, and the TLV length field is never explicitly asserted. tokens/token-2022/mint-close-authority/pinocchio/tests/test.ts — the test coverage is narrow; using distinct keypairs for mint authority and close authority would catch account-ordering mistakes. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client as Client (test.ts)
participant Program as Pinocchio Program
participant System as System Program
participant T22 as Token-2022 Program
Client->>Program: "CreateMint(decimals=9)"
Program->>System: "CreateAccount space=202 owner=Token-2022"
System-->>Program: mint account funded
Program->>T22: InitializeMintCloseAuthority variant 25
T22-->>Program: MintCloseAuthority TLV written
Program->>T22: InitializeMint variant 0
T22-->>Program: base mint fields initialized
Program-->>Client: Ok(())
%%{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 as Client (test.ts)
participant Program as Pinocchio Program
participant System as System Program
participant T22 as Token-2022 Program
Client->>Program: "CreateMint(decimals=9)"
Program->>System: "CreateAccount space=202 owner=Token-2022"
System-->>Program: mint account funded
Program->>T22: InitializeMintCloseAuthority variant 25
T22-->>Program: MintCloseAuthority TLV written
Program->>T22: InitializeMint variant 0
T22-->>Program: base mint fields initialized
Program-->>Client: Ok(())
Reviews (1): Last reviewed commit: "add pinocchio token-2022 mint-close-auth..." | Re-trigger Greptile |
| 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()); | ||
| }); |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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!
| let [mint_account, mint_authority, close_authority, payer, rent_sysvar, _system_program, _token_program] = | ||
| accounts | ||
| else { | ||
| return Err(ProgramError::NotEnoughAccountKeys); | ||
| }; |
There was a problem hiding this comment.
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.
Adds a Pinocchio port of the
tokens/token-2022/mint-close-authorityexample, alongside the existinganchorandnativeversions.What it does
A single instruction creates an SPL Token-2022 mint carrying the
MintCloseAuthorityextension, so a designated authority can later close the mint and reclaim its rent.Notes
Token-2022 has no Pinocchio wrapper crate (
pinocchio-tokentargets the legacy SPL Token program), so the Token-2022 instructions are built by hand and CPI'd:CreateAccountfor the mint, sized to202bytes (baseAccountlength 165 + account-type byte + oneMintCloseAuthorityTLV entry) and owned by the Token-2022 program.InitializeMintCloseAuthority(variant25) — must run before the mint is initialized.InitializeMint(variant0).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 typeMintCloseAuthority) and stored close authority were written correctly.