Skip to content

add pinocchio pda-mint-authority example#612

Open
MarkFeder wants to merge 5 commits into
solana-foundation:mainfrom
MarkFeder:tokens-pda-mint-authority-pinocchio
Open

add pinocchio pda-mint-authority example#612
MarkFeder wants to merge 5 commits into
solana-foundation:mainfrom
MarkFeder:tokens-pda-mint-authority-pinocchio

Conversation

@MarkFeder

Copy link
Copy Markdown
Contributor

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

What it does

A program-derived address — not a wallet — is the mint and freeze authority for every NFT this program creates. Three instructions, dispatched by a leading discriminator byte (matching the native MyInstruction enum):

  • Init (0) — creates the mint-authority PDA ([b"mint_authority"]), signed by its own seeds, and persists the canonical bump in the account.
  • Create (1) — creates a 0-decimal SPL mint whose authority is the PDA, then attaches a Metaplex metadata account via a hand-rolled CreateMetadataAccountV3 CPI. The metadata CPI is authorized with the PDA's seeds via invoke_signed.
  • Mint (2) — creates the payer's associated token account (idempotent), mints the single token, then creates the master edition via a hand-rolled CreateMasterEditionV3 CPI (max_supply = Some(1)). Both the MintTo and master-edition CPIs are signed by the PDA.

The new building block here versus the other token examples is PDA-as-signer: Init, the metadata CPI, the MintTo, and the master-edition CPI all sign as the PDA using pinocchio::cpi::{Seed, Signer} and invoke_signed, rather than relying on a wallet signature. The bump recorded by Init is read back from the PDA account to rebuild the signer seeds without re-deriving the address on-chain.

Tests

tests/test.ts runs under solana-bankrun, loading the program plus the Token Metadata program (dumped from mainnet into tests/fixtures by prepare.mjs). Three cases:

  • Init asserts the PDA account is owned by the program and stores the expected bump.
  • Create asserts the mint is owned by the Token program and the metadata account is owned by Token Metadata and contains the NFT name.
  • Mint asserts the ATA holds exactly 1 token and the master edition account exists and is owned by Token Metadata (proving the PDA-signed CreateMasterEditionV3 CPI succeeded).

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Pinocchio port of the tokens/pda-mint-authority example alongside the existing anchor and native implementations. The program uses a program-derived address ([b"mint_authority"]) as the mint and freeze authority for every NFT it creates, demonstrating the PDA-as-signer pattern via invoke_signed with pinocchio::cpi::{Seed, Signer}.

  • Three instructions are dispatched by a leading discriminator byte: Init (0) creates and persists the PDA with its canonical bump; Create (1) initializes a zero-decimal SPL mint and attaches a hand-rolled Metaplex CreateMetadataAccountV3 CPI; Mint (2) creates the ATA idempotently, mints the single token, and issues a CreateMasterEditionV3 CPI — all signed by the PDA via invoke_signed.
  • Tests use litesvm (not solana-bankrun as the PR description states) to assert correct ownership, bump persistence, token balance, and master edition creation across the three instructions.

Confidence Score: 5/5

This PR is safe to merge; it is a new self-contained example with no changes to existing functionality.

All three instructions correctly recover and verify the PDA bump, the hand-rolled Metaplex CPI account layouts match the Metaplex V3 spec, the invoke_signed signer seeds are correctly scoped, and the integration tests cover the full Init → Create → Mint sequence. No existing code paths are modified.

No files require special attention. The program logic, state layout, and test coverage are all consistent with the other pinocchio examples in the repository.

Important Files Changed

Filename Overview
tokens/pda-mint-authority/pinocchio/program/src/instructions/create.rs Correctly implements PDA bump recovery, SPL mint initialization, and a hand-rolled CreateMetadataAccountV3 CPI with invoke_signed; account layout matches Metaplex V3 expectations.
tokens/pda-mint-authority/pinocchio/program/src/instructions/mint.rs Correctly creates the ATA (idempotent), mints one token via PDA-signed MintTo, and issues a CreateMasterEditionV3 CPI with max_supply = Some(1) matching the native example.
tokens/pda-mint-authority/pinocchio/program/src/instructions/init.rs Creates the mint-authority PDA via invoke_signed, persists the canonical bump in the first account byte; follows established pinocchio escrow pattern for bump handling.
tokens/pda-mint-authority/pinocchio/program/src/state.rs Simple one-field struct serialized into the first byte; ACCOUNT_SPACE = 16 deliberately mirrors the native example layout with 15 unused bytes.
tokens/pda-mint-authority/pinocchio/tests/test.ts Three sequential integration tests using litesvm covering Init, Create, and Mint; assertions cover PDA ownership, bump persistence, token balance, and master edition existence.
tokens/pda-mint-authority/pinocchio/program/src/instructions/mod.rs Defines shared constants (MINT_SIZE, TOKEN_DECIMALS, TOKEN_METADATA_PROGRAM_ID) and Borsh string parser; parsing is safe with proper checked slicing.
tokens/pda-mint-authority/pinocchio/prepare.mjs Dumps the Metaplex Token Metadata program from mainnet using per-command -um flag; the previously flagged global config mutation is fixed.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Program as PDA Mint Auth Program
    participant System as System Program
    participant Token as Token Program
    participant ATA as Assoc. Token Program
    participant Meta as Token Metadata Program
    participant PDA as mint_authority PDA

    Client->>Program: Init(bump)
    Program->>System: "CreateAccount(pda, space=16, owner=Program)"
    Note over Program,PDA: invoke_signed with [b"mint_authority", bump]
    Program->>PDA: write bump to data[0]

    Client->>Program: Create(name, symbol, uri)
    Program->>PDA: read bump from data[0]
    Program->>System: "CreateAccount(mint, space=82, owner=Token)"
    Program->>Token: "InitializeMint2(mint_authority=PDA, decimals=0)"
    Program->>Meta: CreateMetadataAccountV3(name, symbol, uri)
    Note over Program,Meta: invoke_signed - PDA signs as mint authority

    Client->>Program: Mint
    Program->>PDA: read bump from data[0]
    Program->>ATA: "CreateIdempotent(wallet=payer)"
    Program->>Token: "MintTo(amount=1)"
    Note over Program,Token: invoke_signed - PDA signs
    Program->>Meta: "CreateMasterEditionV3(max_supply=Some(1))"
    Note over Program,Meta: invoke_signed - PDA signs as update + mint authority
Loading

Reviews (7): Last reviewed commit: "pda-mint-authority pinocchio: use offici..." | Re-trigger Greptile

Comment on lines +22 to +25
try {
mkdirSync(outputDir, { recursive: true });
// Point the Solana CLI at mainnet, where the canonical program lives.
execSync("solana config set -um", { stdio: "inherit" });

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 solana config set -um is never reverted

pnpm install runs this script as a postinstall hook, permanently redirecting the developer's Solana CLI cluster to mainnet. A developer who normally works against devnet or localnet will silently end up with a wrong cluster config after installing. Saving the current cluster with solana config get json_rpc_url, pointing to mainnet only for the dump, then restoring it afterward would avoid the side-effect.

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 1cfe23d. prepare.mjs now dumps the program with a per-command solana program dump -um … instead of solana config set -um, so pnpm install no longer mutates the developer's global Solana CLI cluster.

Comment on lines +30 to +37
let bump = *data.first().ok_or(ProgramError::InvalidInstructionData)?;

// Verify the supplied account is the canonical PDA for this bump.
let pda = derive_address(
&[MintAuthorityPda::SEED_PREFIX],
Some(bump),
program_id.as_array(),
);

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 Non-canonical bump is accepted without validation

The bump is taken directly from instruction data and passed to derive_address with Some(bump), which computes a PDA for whatever bump value was supplied rather than requiring the canonical one. A caller who deliberately (or accidentally) provides a non-canonical bump will create the mint-authority account at a different address than findProgramAddressSync would derive client-side. Downstream clients that recompute the PDA without knowing which bump was stored will then resolve a different address and be unable to interact with the mints this program created. Since pinocchio_pubkey::derive_address with bump: None finds the canonical bump on-chain, using it here would prevent this class of error.

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.

Leaving this as-is, for two reasons:

  1. Consistency with the established pinocchio pattern. This is exactly what the already-merged tokens/escrow/pinocchio does (make_offer.rs): take the bump from instruction data, then derive_address(&[seed], Some(bump), program_id) and reject the tx unless the supplied account equals that PDA. Every pinocchio example in the repo follows this; matching it keeps the teaching examples uniform.

  2. Toolchain constraint. Deriving the canonical bump on-chain needs find_program_address/create_program_address, whose off-target implementation in solana-address is gated behind the curve25519 feature. CI lint runs cargo clippy -- -D warnings on the host target (no --target sbf), so referencing it there fails to compile unless we pull in the curve25519 dependency — which the lightweight pinocchio stack intentionally avoids. derive_address (from pinocchio-pubkey) is the host-compatible primitive, and it only derives for a given bump.

On safety: the supplied mint-authority account is validated against derive_address(Some(bump)), and create/mint re-derive the signer seeds from the bump persisted in that account, so the program is internally consistent. A client that deliberately passes a non-canonical bump only affects its own address derivation; the test (and any normal client) sources the bump from findProgramAddressSync, which always returns the canonical one.

@MarkFeder
MarkFeder force-pushed the tokens-pda-mint-authority-pinocchio branch from 1cfe23d to 0be01f1 Compare July 8, 2026 21:13
MarkFeder and others added 2 commits July 9, 2026 09:31
Use 'solana program dump -um' instead of 'solana config set -um', so
running pnpm install no longer permanently switches the developer's
Solana CLI cluster to mainnet.
@MarkFeder
MarkFeder force-pushed the tokens-pda-mint-authority-pinocchio branch from 0be01f1 to 0f6fa01 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 🙏

Move the async bankrun setup out of the `describe` callback and into a
`before` hook so Mocha collects the `it` blocks (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` (both the
mint and the PDA account) 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 requested a review from dev-jodee as a code owner July 15, 2026 21:27

@dev-jodee dev-jodee left a comment

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.

Will require changes based on #624 final version

Apply the kit + litesvm template from the mint-close-authority example:
build and sign transactions with @solana/kit and run them on litesvm,
dropping @solana/web3.js and solana-bankrun entirely. The mint-authority PDA
and the metadata/master-edition/ATA addresses are derived with
getProgramDerivedAddress; the Metaplex Token Metadata program is still dumped
from mainnet by prepare.mjs and loaded into LiteSVM via addProgramFromFile.

- deps: drop @solana/web3.js + solana-bankrun, add litesvm; pin @solana/kit
  to ^6.10.0 (litesvm's kit major) so there is a single kit in the tree
- tsconfig: bump typescript to ^5 and lib to es2022+dom (kit's types), add
  @types/node; the suite is type-clean under tsc --noEmit
@MarkFeder
MarkFeder requested a review from dev-jodee July 21, 2026 23:09
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Applied the kit + litesvm template from #624 here as well. The test now builds and signs transactions with @solana/kit and runs them on litesvm, so @solana/web3.js and solana-bankrun are dropped entirely. Pinned @solana/kit to ^6.10.0 (litesvm's kit major) for a single kit in the tree, and bumped typescript/tsconfig (es2022 + dom, @types/node) so the suite is type-clean under tsc --noEmit. Verified locally: cargo build-sbf + ts-mocha3 passing, plus tsc --noEmit and biome clean. Commit is signed.

Applies dev-jodee's solana-foundation#624 review refinements on top of the kit + litesvm test:

- program: compute rent with Rent::get()?.try_minimum_balance(..)? in both the
  Init (PDA account) and Create (mint) instructions, instead of the integer-math
  workaround (only needed to dodge the f64 opcode the old bankrun VM rejected;
  litesvm runs the real syscall).
- test: source the token, associated-token and system program ids from the
  official @solana-program/token and @solana-program/system packages, and read
  the minted amount with getTokenDecoder().decode(...).amount instead of a raw
  byte offset. Token Metadata has no official @solana-program client, so its id
  stays hand-rolled.
- tsconfig: moduleResolution bundler for the packages' subpath exports.

Verified locally: cargo build-sbf + ts-mocha -> 3 passing, tsc --noEmit and
biome clean, frozen-lockfile OK.
@MarkFeder

Copy link
Copy Markdown
Contributor Author

Applied the same refinements @dev-jodee asked for on #624 (now merged):

  • program: rent is computed with Rent::get()?.try_minimum_balance(..)? in both the Init (PDA account) and Create (mint) instructions (the integer-math workaround was only needed to dodge the f64 opcode the old bankrun VM rejected; litesvm runs the real syscall).
  • test: the token, associated-token and system program ids now come from the official @solana-program/token / @solana-program/system packages, and the minted amount is read with getTokenDecoder().decode(...).amount instead of a raw byte offset. Token Metadata has no official @solana-program client, so its id stays hand-rolled.
  • tsconfig: moduleResolution: bundler for the packages' subpath exports.

cargo build-sbf + ts-mocha3 passing; tsc, biome, fmt and clippy clean; frozen-lockfile OK. Commit is signed.

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