Skip to content

fix(pda-mint-authority): gate Mint behind the creator in native and pinocchio - #6

Open
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/pda-mint-authority-gate-native-pinocchio
Open

fix(pda-mint-authority): gate Mint behind the creator in native and pinocchio#6
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/pda-mint-authority-gate-native-pinocchio

Conversation

@SwineCoder101

Copy link
Copy Markdown
Owner

Bug: anyone can mint the NFT a creator paid for (native, pinocchio)

Mint in the native and pinocchio variants has no authorization check. Between a creator's Create and Mint, any wallet can call Mint for that mint and receive the one-of-one NFT in its own associated token account; the Master Edition CPI then hands the mint authority to Metaplex, so the creator's own Mint fails afterwards. This is theft of an asset the creator paid rent and metadata fees to set up, exploitable by any unprivileged wallet that watches the chain. solana-foundation#692 fixed the same missing gate in the anchor variant but assumed the two siblings were safe because the edition caps supply at 1 — the cap prevents inflation, not the wrong recipient.

Affected

  • native/
    • tokens/pda-mint-authority/native/program/src/instructions/mint.rs
    • tokens/pda-mint-authority/native/program/src/instructions/create.rs
    • tokens/pda-mint-authority/native/program/src/state/mod.rs
    • tokens/pda-mint-authority/native/ts/instructions/{create,mint}.ts
    • tokens/pda-mint-authority/native/tests/test.ts
  • pinocchio/
    • tokens/pda-mint-authority/pinocchio/program/src/instructions/mint.rs
    • tokens/pda-mint-authority/pinocchio/program/src/instructions/create.rs
    • tokens/pda-mint-authority/pinocchio/program/src/state.rs
    • tokens/pda-mint-authority/pinocchio/tests/test.ts

The anchor variant was already fixed in solana-foundation#692 and is untouched.

Functionality

Init creates the program's global mint-authority PDA ([b"mint_authority"]). Create has the payer fund a new mint whose mint/freeze authority is that PDA and attaches Metaplex metadata. Mint mints the single token into the payer's ATA, signed by the PDA via invoke_signed, then creates the Master Edition, which transfers the mint authority to Metaplex and caps supply at 1.

The bug

Before this change the only check in Mint was that mint_authority is the program PDA (native mint.rs:30-32, pinocchio mint.rs:52-57). Nothing linked the caller to the mint:

  • native mint.rs:34-50 creates the ATA with create_associated_token_account(payer, payer, mint, ...) and mint.rs:59-70 mints to it. payer is whoever signed the transaction.
  • pinocchio mint.rs:64-83 does the same with CreateIdempotent { wallet: payer, ... } followed by MintTo.

mint_account is a plain writable account; any mint whose authority is the PDA is accepted. So the sequence creator: Create(mint)outsider: Mint(mint) succeeds and the NFT lands in the outsider's ATA. The subsequent creator: Mint(mint) fails with SPL Token error 4 (owner does not match) because the edition now holds the authority.

Reproduce

cd tokens/pda-mint-authority/native    && pnpm install --frozen-lockfile && pnpm build-and-test
cd tokens/pda-mint-authority/pinocchio && pnpm install --frozen-lockfile && pnpm build-and-test

Test: Rejects a Mint from a wallet that did not create the NFT (both variants). Against the unpatched program:

  NFT Minter
    ✔ Init Mint Authority PDA
    ✔ Create an NFT!
    1) Rejects a Mint from a wallet that did not create the NFT
    2) Mint the NFT to your wallet!

  1) NFT Minter
       Rejects a Mint from a wallet that did not create the NFT:
     AssertionError: an unrelated wallet minted the NFT created by the payer

  2) NFT Minter
       Mint the NFT to your wallet!:
     AssertionError: transaction failed: FailedTransactionMetadata(... err: InstructionError(0, Custom(4)) ...
       "Program log: Minting NFT to associated token account...",
       "Program log: Error: owner does not match", ...
  PDA Mint Authority (Pinocchio)
    ✔ Initialize the mint authority PDA!
    ✔ Create an NFT!
    1) Rejects a Mint from a wallet that did not create the NFT
    2) Mint the NFT to your wallet!

  1) ... AssertionError: an unrelated wallet minted the NFT created by the payer:
     expected TransactionMetadata{} to be an instance of FailedTransactionMetadata
  2) ... Error: Transaction failed: TransactionErrorInstructionError { index: 0, error: InstructionErrorCustom { code: 4 } }

Fix

Mirrors solana-foundation#692's MintConfig approach, made per-mint because these variants create a fresh mint per Create:

  • New MintConfig { bump, admin } state (33 bytes) at PDA [b"mint_config", mint] (native state/mod.rs, pinocchio state.rs).
  • Create takes the mint_config account after metadata_account, verifies its derivation, creates it with invoke_signed, and records payer as admin (native create.rs:102-122, pinocchio create.rs:105-126). The system-program CPI already requires payer to have signed.
  • Mint takes mint_config after mint_authority, verifies derivation and program ownership, requires payer to be a signer, and rejects with ProgramError::IncorrectAuthority unless payer == admin (native mint.rs:37-50, pinocchio mint.rs:60-75).

Wire format change: one extra account on Create and Mint in each variant; instruction data is unchanged. The native TS client (ts/instructions/create.ts, ts/instructions/mint.ts) gained a mintConfig parameter and both test suites derive and pass it. Create also now asserts the recorded creator bytes, and Mint asserts the token owner is the creator and mint supply is 1.

Verification

Same commands as above, after the fix:

  NFT Minter
    ✔ Init Mint Authority PDA
    ✔ Create an NFT!
    ✔ Rejects a Mint from a wallet that did not create the NFT
    ✔ Mint the NFT to your wallet!
  4 passing

  PDA Mint Authority (Pinocchio)
    ✔ Initialize the mint authority PDA!
    ✔ Create an NFT!
    ✔ Rejects a Mint from a wallet that did not create the NFT
    ✔ Mint the NFT to your wallet!
  4 passing

The outsider's transaction now fails with InstructionError(0, IncorrectAuthority) in both variants, its ATA is never created, mint supply stays 0, and no edition account exists until the creator mints. cargo fmt --check, cargo clippy -p pda-mint-authority-native-program -p pda-mint-authority-pinocchio-program -- -D warnings, tsc --noEmit in both projects, and prettier all pass.

… in native and pinocchio

Add a test to each variant where an unrelated keypair calls Mint for a
mint the creator just created. It currently succeeds: the NFT lands in
the outsider's ATA and the master edition takes the mint authority, so
the creator's own Mint then fails. Also assert that the creator's Mint
lands the token in the creator's ATA with supply 1.
…inocchio

Mint had no authorization: anyone could call it for a mint the creator
had just set up and receive the one-of-one NFT in their own ATA, after
which the creator's own Mint fails because the Master Edition took the
mint authority. Mirror solana-foundation#692's MintConfig approach per mint: Create
records the payer in a [b"mint_config", mint] PDA and Mint requires that
wallet to sign, rejecting others with IncorrectAuthority. One extra
account on Create and Mint; instruction data is unchanged. Update the
native TS client and both test suites for the new account.
@SwineCoder101
SwineCoder101 force-pushed the fix/pda-mint-authority-gate-native-pinocchio branch from 0fc8444 to 16eb75b Compare August 27, 2026 12:23
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