{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreieszru44rikn6abyaiwlweyjz227wkzc24rnavdnne6o3tluoyhvi",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpr4v6ypnob2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreicc5pxzpgsrbsuf36j2tn3upszmhoggjyr6uq57sszuouslk5aqai"
},
"mimeType": "image/webp",
"size": 257812
},
"path": "/lymah/cpis-and-pda-signers-what-i-built-and-what-actually-clicked-m6d",
"publishedAt": "2026-07-03T17:45:51.000Z",
"site": "https://dev.to",
"tags": [
"100daysofcode",
"anchor",
"rust",
"solana",
"Cross-Program Invocations — Solana docs",
"CPI with signer seeds — Anchor book",
"System Program transfer",
"Token Interface mint_to",
"PDA signing — Solana cookbook"
],
"textContent": "I spent four days building programs that move SOL and tokens on Solana.\nDay 71 was a simple SOL transfer through a CPI. Day 74 was a mint\nwhose authority belongs to a PDA with no private key — meaning nobody\ncan ever mint outside the program's rules again.\n\nThis post is what I learned between those two points, written for\nsomeone who understands Web2 backend code but has never written a\nSolana program.\n\n## The One Sentence Version\n\nA CPI is a program calling another program. A PDA signer is how a\nprogram proves it authorized that call without holding a private key.\n\nEverything else is details.\n\n## What CPIs Actually Are\n\nIn Web2, your backend calls external APIs. A payment service, a\nmessaging provider, a database driver. You pass credentials, the\nservice does something, you get a result.\n\nOn Solana, programs call other programs the same way — except the\n\"credentials\" are not an API key. They are either a wallet signature\nthat propagated from the outer transaction, or a set of seeds that\nprove the calling program derived a specific address.\n\nThe mechanism is `CpiContext`:\n\n\n\n let cpi_ctx = CpiContext::new(\n ctx.accounts.system_program.key(), // which program to call\n Transfer { // the accounts it needs\n from: ctx.accounts.user.to_account_info(),\n to: ctx.accounts.vault.to_account_info(),\n },\n );\n transfer(cpi_ctx, amount)?; // the instruction\n\n\nThree things: the program to call, the accounts it needs, the\ninstruction to run. The `?` makes it atomic — if the CPI fails,\nthe whole transaction rolls back.\n\n## The Two Cases\n\n**Case 1: the wallet signs**\n\nWhen a user signs the outer transaction, that signature propagates\nautomatically into any CPI that uses the wallet as an authority.\nYou do not have to do anything special.\n\n\n\n // User signed the tx → their wallet can authorize this CPI\n transfer(CpiContext::new(system_program, Transfer {\n from: user_wallet,\n to: recipient,\n }), amount)?;\n\n\nThis is what I used on Day 71 (SOL transfer) and Day 72 (token mint).\nThe program is a policy layer — it checks conditions, then forwards\nthe call.\n\n**Case 2: the program signs**\n\nWhen the program needs to authorize something the user never signed\nfor — like returning SOL from a vault or minting a reward — it uses\nPDA signer seeds:\n\n\n\n let signer_seeds: &[&[&[u8]]] = &[&[\n b\"vault\",\n user_key.as_ref(),\n &[bump]\n ]];\n\n let cpi_ctx = CpiContext::new(system_program, Transfer {\n from: vault_pda,\n to: user_wallet,\n }).with_signer(signer_seeds); // ← this is the entire mechanism\n\n transfer(cpi_ctx, amount)?;\n\n\nThe runtime re-derives the PDA from those seeds and your program ID.\nIf the result matches the account you passed in, the CPI is\nauthorized. No private key. No human approval.\n\n## The Vault: Deposit In, Program Signs Out\n\nDay 73 was the clearest demonstration. Two instructions:\n\n\n\n deposit(amount) → user wallet signs, SOL flows INTO the vault PDA\n withdraw(amount) → program signs via seeds, SOL flows OUT of vault PDA\n\n\nThe vault PDA is derived from `[\"vault\", user_pubkey]`. This means:\n\n * Every user has their own vault at a unique address\n * The program can only withdraw to the user whose key is in the seeds\n * Passing a different user's vault produces a seeds mismatch — rejected\n\n\n\nThe test confirmed it:\n\n\n\n vault after deposit: 500000000 lamports\n vault after withdraw: 0 lamports\n ✔ deposits, then the program signs to withdraw\n\n\nAnd the failure test confirmed the protection:\n\n\n\n Impostor trying to withdraw from original user vault...\n ✗ FAILURE 2: AnchorError caused by account: vault.\n Error Code: ConstraintSeeds. Error Number: 2006.\n A seeds constraint was violated.\n\n\nThe seeds are the authorization policy. Not an ownership check in\nthe handler — the derivation itself.\n\n## The PDA Mint Authority: Permanent Program Control\n\nDay 74 was the harder version. I created a Token-2022 mint, then\ntransferred mint authority to a PDA:\n\n\n\n await setAuthority(\n connection,\n payer,\n mint,\n payer,\n AuthorityType.MintTokens,\n mintAuthorityPda, // ← PDA with no private key\n [],\n undefined,\n TOKEN_2022_PROGRAM_ID\n );\n\n\nAfter this transaction, the only way to mint tokens is through\nthe program's `mint_tokens` instruction. No wallet can call\n`mintTo` directly because no private key corresponds to the\nPDA address. The program is the only entity that can reconstruct\nthe signer seeds at runtime.\n\n\n\n Mint authority PDA: 8p6j3X6pBDVf4kzXcgk2VnnN3Jo18tJpjWqraMzguSbC\n Minted base units: 500000000\n ✔ PDA signs the mint CPI — no human holds mint authority\n Confirmed: only the program can mint — PDA has no private key\n\n\n## What This Pattern Unlocks\n\nEvery meaningful DeFi primitive on Solana uses this pattern:\n\nProtocol type | What the PDA controls\n---|---\nVault / escrow | SOL or token release conditions\nAMM | Token pool balances\nLending protocol | Collateral and liquidation\nReward program | When and how much to mint\nDAO treasury | Fund distribution rules\n\nIn every case, the rules live in the program. The PDA enforces\nthat only the program can act. The seeds determine what the program\ncan act on behalf of.\n\n## The Three Failures That Made It Real\n\nI wrote a test suite that deliberately triggered three CPI failures:\n\n**Failure 1 — Insufficient funds:**\nTried to withdraw 5 SOL from a vault holding 0.1 SOL.\n`✗ FAILURE 1: Simulation failed.`\nThe System Program rejected the transfer before it hit the chain.\n\n**Failure 2 — Wrong signer (seeds mismatch):**\nAn impostor wallet tried to withdraw from a different user's vault.\n\n\n\n ✗ FAILURE 2: ConstraintSeeds. Error Number: 2006.\n A seeds constraint was violated.\n\n\nThe PDA derivation produced a different address. Rejected before\nthe handler ran.\n\n**Failure 3 — Wrong program ID:**\nPassed a fake address instead of the System Program.\n\n\n\n ✗ FAILURE 3: InvalidProgramId. Error Number: 3008.\n Program ID was not as expected.\n\n\nAnchor validated the program address before the CPI could execute.\n\nAll three caught cleanly. All three tell you exactly what went wrong.\n\n## What I Would Tell Someone Starting This Week\n\n**The wallet signature propagates automatically.**\nYou do not need `.with_signer` when the user's wallet is the\nauthority. That is only for PDAs.\n\n**`.with_signer` is the entire PDA signing mechanism.**\nEverything else — the CpiContext, the accounts struct, the\ninstruction call — is the same whether a wallet or a PDA signs.\nThe only difference is that one line.\n\n**Store the bump on the account.**\nCompute it once in the `init` instruction with `ctx.bumps.counter`,\nstore it as a field, reuse it on every subsequent call.\nRe-deriving it each time wastes compute.\n\n**The seeds are the authorization policy.**\nA vault derived from `[\"vault\", user_pubkey]` can only be drained\nto that user. You do not need an explicit ownership check — the\nderivation enforces it. If someone passes the wrong account, the\nseeds mismatch and Anchor rejects the transaction.\n\n**`setAuthority` is a point of no return.**\nOnce you transfer mint authority to a PDA, no human can mint outside\nyour program's rules. Make sure those rules are correct before you\ncall it on mainnet.\n\n## Resources\n\n * Cross-Program Invocations — Solana docs\n * CPI with signer seeds — Anchor book\n * System Program transfer\n * Token Interface mint_to\n * PDA signing — Solana cookbook\n\n\n\n_Part of *_ #100DaysOfSolana*_. Building every day._",
"title": "CPIs and PDA Signers: What I Built and What Actually Clicked"
}