add pinocchio pda-mint-authority example#612
Conversation
Greptile SummaryThis PR adds a Pinocchio port of the
Confidence Score: 5/5Safe to merge — all three instruction handlers, the hand-rolled Metaplex CPIs, and the bankrun test suite are logically correct with no data-loss or security-bypass paths introduced. The three instructions implement a straightforward and well-tested PDA-as-signer flow. Both PDA address verifications are correctly applied in No files require special attention; the most complex logic is in Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant Program as PDA-Mint-Authority Program
participant SysProg as System Program
participant TokProg as Token Program
participant ATA as Associated Token Program
participant Meta as Token Metadata Program
Note over Client, Meta: Instruction 0 - Init
Client->>Program: Init(bump)
Program->>Program: derive PDA and verify address
Program->>SysProg: "CreateAccount(PDA, space=16, owner=Program) signed by PDA seeds"
Program->>Program: Write bump into PDA account byte 0
Note over Client, Meta: Instruction 1 - Create
Client->>Program: Create(name, symbol, uri)
Program->>Program: Read bump from PDA, re-derive and verify PDA address
Program->>SysProg: "CreateAccount(mint, space=82, owner=TokenProgram)"
Program->>TokProg: "InitializeMint2(decimals=0, mint_auth=PDA, freeze_auth=PDA)"
Program->>Meta: CreateMetadataAccountV3 signed by PDA seeds
Note over Client, Meta: Instruction 2 - Mint
Client->>Program: Mint
Program->>Program: Read bump from PDA, re-derive and verify PDA address
Program->>ATA: CreateIdempotent(ATA for payer)
Program->>TokProg: "MintTo(amount=1) signed by PDA seeds"
Program->>Meta: "CreateMasterEditionV3(max_supply=1) signed by PDA seeds"
%%{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 as PDA-Mint-Authority Program
participant SysProg as System Program
participant TokProg as Token Program
participant ATA as Associated Token Program
participant Meta as Token Metadata Program
Note over Client, Meta: Instruction 0 - Init
Client->>Program: Init(bump)
Program->>Program: derive PDA and verify address
Program->>SysProg: "CreateAccount(PDA, space=16, owner=Program) signed by PDA seeds"
Program->>Program: Write bump into PDA account byte 0
Note over Client, Meta: Instruction 1 - Create
Client->>Program: Create(name, symbol, uri)
Program->>Program: Read bump from PDA, re-derive and verify PDA address
Program->>SysProg: "CreateAccount(mint, space=82, owner=TokenProgram)"
Program->>TokProg: "InitializeMint2(decimals=0, mint_auth=PDA, freeze_auth=PDA)"
Program->>Meta: CreateMetadataAccountV3 signed by PDA seeds
Note over Client, Meta: Instruction 2 - Mint
Client->>Program: Mint
Program->>Program: Read bump from PDA, re-derive and verify PDA address
Program->>ATA: CreateIdempotent(ATA for payer)
Program->>TokProg: "MintTo(amount=1) signed by PDA seeds"
Program->>Meta: "CreateMasterEditionV3(max_supply=1) signed by PDA seeds"
Reviews (5): Last reviewed commit: "pda-mint-authority pinocchio: make bankr..." | Re-trigger Greptile |
| try { | ||
| mkdirSync(outputDir, { recursive: true }); | ||
| // Point the Solana CLI at mainnet, where the canonical program lives. | ||
| execSync("solana config set -um", { stdio: "inherit" }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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(), | ||
| ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Leaving this as-is, for two reasons:
-
Consistency with the established pinocchio pattern. This is exactly what the already-merged
tokens/escrow/pinocchiodoes (make_offer.rs): take the bump from instruction data, thenderive_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. -
Toolchain constraint. Deriving the canonical bump on-chain needs
find_program_address/create_program_address, whose off-target implementation insolana-addressis gated behind thecurve25519feature. CI lint runscargo clippy -- -D warningson 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(frompinocchio-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.
1cfe23d to
0be01f1
Compare
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.
0be01f1 to
0f6fa01
Compare
|
@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.
Adds a Pinocchio port of the
tokens/pda-mint-authorityexample, alongside the existinganchorandnativeversions.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
MyInstructionenum):0) — creates the mint-authority PDA ([b"mint_authority"]), signed by its own seeds, and persists the canonical bump in the account.1) — creates a 0-decimal SPL mint whose authority is the PDA, then attaches a Metaplex metadata account via a hand-rolledCreateMetadataAccountV3CPI. The metadata CPI is authorized with the PDA's seeds viainvoke_signed.2) — creates the payer's associated token account (idempotent), mints the single token, then creates the master edition via a hand-rolledCreateMasterEditionV3CPI (max_supply = Some(1)). Both theMintToand 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, theMintTo, and the master-edition CPI all sign as the PDA usingpinocchio::cpi::{Seed, Signer}andinvoke_signed, rather than relying on a wallet signature. The bump recorded byInitis read back from the PDA account to rebuild the signer seeds without re-deriving the address on-chain.Tests
tests/test.tsruns undersolana-bankrun, loading the program plus the Token Metadata program (dumped from mainnet intotests/fixturesbyprepare.mjs). Three cases:CreateMasterEditionV3CPI succeeded).