solanaanchor-solanasolana-program-library

Solana Anchor: CPI with PDA


I'm trying to create an Mint having my program as authority and I'm struggling to get my CPI calls right.

Here is a toy example of what I have so far:

use anchor_lang::prelude::*;
use anchor_spl::token::{
    self, set_authority, spl_token::instruction::AuthorityType, SetAuthority, Token,
};
use program::MintTest;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod mint_test {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let bump = *ctx.bumps.get("mint").unwrap();
        let seeds = vec![bump];
        let seeds = vec![b"some-seed".as_ref(), seeds.as_slice()];
        let seeds = vec![seeds.as_slice()];
        let seeds = seeds.as_slice(); // ❓ slightly unrelated but I'd love to understand why all this nesting is required 🤔

        let cpi_program = ctx.accounts.token_program.to_account_info();
        let cpi_accounts = SetAuthority {
            account_or_mint: ctx.accounts.mint.to_account_info(),
            current_authority: ctx.accounts.program.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds);
        set_authority(cpi_ctx, AuthorityType::MintTokens, None)?; // ❌ This fails 🙁

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = program,
        seeds = [b"some-seed".as_ref()],
        bump,
    )]
    pub mint: Account<'info, token::Mint>,

    #[account(mut)]
    pub signer: Signer<'info>,
    pub token_program: Program<'info, Token>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
    pub program: Program<'info, MintTest>,
}

The mint is created correctly, but the any CPI call fails with Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: Cross-program invocation with unauthorized signer or writable account (set_authority is just an example, I tried other CPIs like mint_to without more success 😔).

It does work if I set the TX signer as authority so I assume I'm doing something wrong with my signer seeds but I just can't figure it out and I have been stuck on this for hours now.

Here is my TS test as well:

import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { MintTest } from "../target/types/mint_test";

describe("mint-test", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.MintTest as Program<MintTest>;

  it("Is initialized!", async () => {
    const [mint] = await anchor.web3.PublicKey.findProgramAddress(
      [Buffer.from("some-seed")],
      program.programId
    );

    const tx = await program.methods
      .initialize()
      .accounts({
        mint,
        program: program.programId,
      })
      .rpc();

    console.log("Your transaction signature", tx);
  });
});

Thank you in advance for your help 😇


Solution

  • So, it just seems like my understanding of all this was a bit off. A program just cannot sign, so our program can't be our mint's authority.

    However, we can assign a PDA as the owner of our mint and use the seeds used to find that PDA's address to "sign" instructions.

    The following works:

    use anchor_lang::prelude::*;
    use anchor_spl::token::{
        self, set_authority, spl_token::instruction::AuthorityType, SetAuthority, Token,
    };
    
    declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
    
    #[program]
    pub mod mint_test {
        use super::*;
    
        pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
            let bump = *ctx.bumps.get("auth").unwrap();
            let seeds = vec![bump];
            let seeds = vec![b"auth".as_ref(), seeds.as_slice()];
            let seeds = vec![seeds.as_slice()];
            let seeds = seeds.as_slice();
    
            let cpi_program = ctx.accounts.token_program.to_account_info();
            let cpi_accounts = SetAuthority {
                account_or_mint: ctx.accounts.mint.to_account_info(),
                current_authority: ctx.accounts.auth.to_account_info(),
            };
            let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds);
            set_authority(cpi_ctx, AuthorityType::MintTokens, None)?;
    
            Ok(())
        }
    }
    
    #[account]
    pub struct Auth {}
    
    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[account(
            init,
            space = 8,
            payer = signer,
            seeds = [b"auth".as_ref()],
            bump,
        )]
        pub auth: Account<'info, Auth>,
    
        #[account(
            init,
            payer = signer,
            mint::decimals = 0,
            mint::authority = auth,
            seeds = [b"some-seed".as_ref()],
            bump,
        )]
        pub mint: Account<'info, token::Mint>,
    
        #[account(mut)]
        pub signer: Signer<'info>,
        pub token_program: Program<'info, Token>,
        pub system_program: Program<'info, System>,
        pub rent: Sysvar<'info, Rent>,
    }
    

    And the test:

    import * as anchor from "@project-serum/anchor";
    import { Program } from "@project-serum/anchor";
    import { MintTest } from "../target/types/mint_test";
    
    describe("mint-test", () => {
      anchor.setProvider(anchor.AnchorProvider.env());
    
      const program = anchor.workspace.MintTest as Program<MintTest>;
    
      it("Is initialized!", async () => {
        const [auth] = await anchor.web3.PublicKey.findProgramAddress(
          [Buffer.from("auth")],
          program.programId
        );
    
        const [mint] = await anchor.web3.PublicKey.findProgramAddress(
          [Buffer.from("some-seed")],
          program.programId
        );
    
        const tx = await program.methods
          .initialize()
          .accounts({
            mint,
            auth,
          })
          .rpc();
    
        console.log("Your transaction signature", tx);
      });
    });
    

    Hopefully that's helpful to someone.