Skip to content

add pinocchio pda-mint-authority example#612

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

add pinocchio pda-mint-authority example#612
MarkFeder wants to merge 2 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, implementing three on-chain instructions (Init, Create, Mint) that use a program-derived address as the mint and freeze authority for an NFT, alongside a solana-bankrun test suite that exercises the full flow.

  • Init creates the mint-authority PDA, stores the canonical bump in its first data byte, and uses invoke_signed with the PDA seeds so the account creation is self-authorized.
  • Create reads the stored bump back to rebuild signer seeds, initializes a 0-decimal SPL mint, and calls the Metaplex CreateMetadataAccountV3 CPI via a hand-rolled Borsh serializer.
  • Mint creates the payer's ATA idempotently, mints exactly one token with the PDA as signer, and finalizes with a CreateMasterEditionV3 CPI — all three CPIs authorized with the same PDA seeds rebuilt from the stored bump.

Confidence Score: 5/5

Safe to merge; the three-instruction flow is correctly implemented, CPI encoding matches Metaplex layout, and the bankrun tests cover the full Init→Create→Mint sequence end-to-end.

The instruction logic, PDA signer seed construction, Borsh encoding for both Metaplex CPIs, and ATA idempotency are all correct. The two comments left are educational suggestions for an example program rather than defects in the on-chain behavior.

No files require special attention beyond the two inline suggestions; create.rs and state.rs have minor points worth a second look before this is used as a copy-paste starting point.

Important Files Changed

Filename Overview
tokens/pda-mint-authority/pinocchio/program/src/instructions/init.rs Creates the mint-authority PDA; accepts any bump from the caller and validates it by re-deriving the address, which permits non-canonical bumps (flagged in prior thread)
tokens/pda-mint-authority/pinocchio/program/src/instructions/create.rs Creates SPL mint (0 decimals) and attaches Metaplex metadata via hand-rolled CPI; reads the PDA bump without verifying account ownership against program_id
tokens/pda-mint-authority/pinocchio/program/src/instructions/mint.rs Mints NFT to payer ATA and creates master edition; reads bump from mint_authority without owner verification; account layout and CPI encoding look correct
tokens/pda-mint-authority/pinocchio/program/src/state.rs MintAuthorityPda stores only a single bump byte but allocates ACCOUNT_SPACE=16; comment citing native (8+8) is accurate for parity but the 15 wasted bytes add unnecessary rent
tokens/pda-mint-authority/pinocchio/program/src/processor.rs Clean dispatch on first discriminator byte; correctly strips discriminator before forwarding args to each handler
tokens/pda-mint-authority/pinocchio/program/src/instructions/mod.rs Borsh string parser handles malformed length prefix safely via get+ok_or; constants and CreateTokenArgs look correct
tokens/pda-mint-authority/pinocchio/tests/test.ts Three bankrun tests cover Init, Create, and Mint; assertions cover PDA ownership, bump storage, mint ownership, metadata name, ATA balance, and master edition ownership
tokens/pda-mint-authority/pinocchio/prepare.mjs Uses solana program dump -um flag to fetch token_metadata from mainnet without mutating global CLI config; errors are caught and logged non-fatally

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Program
    participant SystemProgram
    participant TokenProgram
    participant ATAProgram
    participant MetaplexProgram

    Note over Client,MetaplexProgram: Instruction 0 - Init
    Client->>Program: Init(bump)
    Program->>Program: derive_address seeds+bump, verify account matches
    Program->>SystemProgram: CreateAccount invoke_signed with PDA seeds
    Program->>Program: Write bump into PDA data[0]

    Note over Client,MetaplexProgram: Instruction 1 - Create
    Client->>Program: Create(name, symbol, uri)
    Program->>Program: Read bump from PDA data[0], re-derive and verify address
    Program->>SystemProgram: CreateAccount for mint 82 bytes
    Program->>TokenProgram: "InitializeMint2 decimals=0 authority=PDA"
    Program->>MetaplexProgram: CreateMetadataAccountV3 invoke_signed with PDA seeds

    Note over Client,MetaplexProgram: Instruction 2 - Mint
    Client->>Program: Mint
    Program->>Program: Read bump from PDA data[0], re-derive and verify address
    Program->>ATAProgram: CreateIdempotent ATA for payer
    Program->>TokenProgram: "MintTo amount=1 invoke_signed with PDA seeds"
    Program->>MetaplexProgram: "CreateMasterEditionV3 max_supply=1 invoke_signed with PDA seeds"
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 Program
    participant SystemProgram
    participant TokenProgram
    participant ATAProgram
    participant MetaplexProgram

    Note over Client,MetaplexProgram: Instruction 0 - Init
    Client->>Program: Init(bump)
    Program->>Program: derive_address seeds+bump, verify account matches
    Program->>SystemProgram: CreateAccount invoke_signed with PDA seeds
    Program->>Program: Write bump into PDA data[0]

    Note over Client,MetaplexProgram: Instruction 1 - Create
    Client->>Program: Create(name, symbol, uri)
    Program->>Program: Read bump from PDA data[0], re-derive and verify address
    Program->>SystemProgram: CreateAccount for mint 82 bytes
    Program->>TokenProgram: "InitializeMint2 decimals=0 authority=PDA"
    Program->>MetaplexProgram: CreateMetadataAccountV3 invoke_signed with PDA seeds

    Note over Client,MetaplexProgram: Instruction 2 - Mint
    Client->>Program: Mint
    Program->>Program: Read bump from PDA data[0], re-derive and verify address
    Program->>ATAProgram: CreateIdempotent ATA for payer
    Program->>TokenProgram: "MintTo amount=1 invoke_signed with PDA seeds"
    Program->>MetaplexProgram: "CreateMasterEditionV3 max_supply=1 invoke_signed with PDA seeds"
Loading

Reviews (2): Last reviewed commit: "pda-mint-authority pinocchio: dump fixtu..." | 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.

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.
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.

1 participant