user1735752
user1735752

Reputation: 13

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 😇

Upvotes: 0

Views: 1905

Answers (2)

0xGhohun
0xGhohun

Reputation: 1

As stated in this answer https://stackoverflow.com/a/73950605/20494156, program can't sign by itself, the only way is by using PDAs. More information on programs as signers can be found here: https://book.anchor-lang.com/anchor_in_depth/PDAs.html#programs-as-signers

Just wanted to add the following regarding the inner comments question about the seeds nesting, this answer might be helpful: https://solana.stackexchange.com/questions/3002/can-there-be-two-signers-in-an-anchor-cpi.

Basically the idea is that you could send multiple arrays of seeds in case there are multiple PDAs signing.

Upvotes: 0

user1735752
user1735752

Reputation: 13

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.

Upvotes: 0

Related Questions