security: gate pda-mint-authority's mint_token behind the token's original creator - #692
Conversation
…ginal creator mint_token has no authorization check: any signer can mint an arbitrary amount to itself, since the mint's authority is a program-derived PDA (seeds = [b"mint"]) that signs the MintTo CPI unconditionally for whoever calls the instruction. create_token uses Anchor's `init` constraint, so the mint is created exactly once, globally, as a plain fungible token (decimals 9, no supply cap, no Metaplex Master Edition) — the result is unrestricted, repeatable token inflation by any wallet. This is undocumented, not an intentional simplification: the README only describes the PDA-as-signer mechanic, and the sibling native/pinocchio implementations of this same example mint a supply-1 NFT and hand the mint authority to a Master Edition PDA afterward, which incidentally caps them — the anchor implementation is the only one of the three with no cap and no gate. The existing test only ever calls mint_token as the same wallet that created the token, so the missing check had zero coverage. Adds a MintConfig PDA (seeds = [b"mint_config"]) recording the admin at create_token time, and an Anchor account-level constraint on mint_token requiring the caller to match it. The existing "PDA is both the mint account address and CPI signer" demonstration is untouched. Verified: reverted the fix, rebuilt, and confirmed an unrelated wallet's mint_token call succeeds against the unpatched code; reapplied the fix and confirmed the same call is rejected with Unauthorized. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Greptile SummaryThis PR adds a program-owned MintConfig PDA that records the original token creator and requires that creator to sign every subsequent mint operation.
Confidence Score: 5/5The PR appears safe to merge, with the new creator-only mint authorization consistently initialized, enforced, and covered by a negative test. The canonical program-owned configuration and mint use fixed global PDA seeds, are initialized atomically, and the mint instruction validates the stored administrator against the required signer before reaching the token-program CPI. Important Files Changed
Reviews (1): Last reviewed commit: "security: gate pda-mint-authority's mint..." | Re-trigger Greptile |
|
Hey @dev-jodee — this is ready for review whenever you get a chance. CI is green and Greptile's automated pass found no blocking issues. |
Summary
tokens/pda-mint-authority(anchor) has an unrestricted-minting vulnerability:mint_tokenperforms no authorization check whatsoever. Its only accounts are
payer: Signer(anywallet) and the mint/ATA accounts — the mint's authority is a program-derived PDA
(
seeds = [b"mint"], set up increate_token) that signs theMintToCPI unconditionallyon every call, for whoever happens to call the instruction.
create_tokenuses Anchor'sinitconstraint, so the mint is created exactly once,globally, as a plain SPL fungible token (
mint::decimals = 9, no supply cap, no MetaplexMaster Edition). Combined with the missing check on
mint_token, any wallet can mint anarbitrary amount of this token to itself, repeatedly, forever — unrestricted token supply
inflation, with no cap and no gate.
Why this went unnoticed
changes the mint authority account from the payer to a PDA" — no caveat about missing
access control (unlike this repo's
compression/cnft-vault, which explicitly disclosesits own analogous gap as an intentional proof-of-concept limitation).
create.rs's own comment frames the PDA-as-signer mechanic as the sole teaching point:"demonstrate that the same PDA can be used for both the address of an account and CPI
signing." Access control was never part of the design discussion.
nativeandpinocchioimplementations of this same example mint an NFT(supply 1), then hand the mint/freeze authority to a Metaplex Master Edition PDA — which
incidentally caps them at one mint per account, even without an explicit check. The
anchor implementation departs from this (plain fungible token, no edition, no cap) and
is the only one of the three with unbounded, repeatable minting.
mint_tokenas the same wallet that calledcreate_token— there is no test with a second, unrelated caller, so the missing checkhad zero coverage.
Fix
Adds a
MintConfigPDA (seeds = [b"mint_config"]) that records the admin (the wallet thatcalled
create_token) and gatesmint_tokenbehind it via an Anchor account-levelconstraint (
mint_config.admin == payer.key()). The existing "PDA is both the mint accountaddress and the CPI signer" demonstration — the example's actual teaching point — is
completely untouched; this only adds a small, separate, read-only-checked account
controlling who may trigger the existing mint logic.
Test changes
Added a negative test in
tests/litesvm.test.ts: a second, freshly-generated walletattempts
mint_tokenafter the first wallet'screate_token, asserting it fails withUnauthorized. Verified this reproduces against the unpatched code (reverted the fix,rebuilt, confirmed the unrelated wallet's mint call succeeds), then reapplied the fix and
confirmed it's correctly rejected. The two pre-existing tests pass unchanged — Anchor's
client-side PDA auto-resolution picks up the new
mint_configaccount without needingexplicit wiring in
.accountsPartial(...).Out of scope
createandmint_toare separate instructions, any caller can crank the one-timemint_tobefore the intended recipient, directing a single NFT to themselves instead.This is bounded to one NFT per mint account and requires timing/front-running, unlike the
anchor bug's unbounded, un-timed value creation — flagging it here for visibility, not
fixing it in this PR.
compression/cnft-vault's missing access control is a disclosed, intentional limitationper its own README and not something this audit should "fix" against the authors' stated
intent.
Verification
cargo check,anchor build,cargo fmt -p pda-mint-authority-anchor(scoped — thiscrate is listed in
.github/.workspace-ignore),pnpm exec tsc --noEmit, rootprettier --checkall pass.litesvm.test.tssuite: 3/3 passing.confirmed the new negative test fails against the unpatched code (an unrelated wallet's
mint succeeds), reapplied the patch, confirmed the full suite passes green.