Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use anchor_lang::prelude::*;

#[error_code]
pub enum TokenMinterError {
#[msg("Only the admin recorded at token creation may mint")]
Unauthorized,
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// In this example the same PDA is used as both the address of the mint account and the mint authority
// This is to demonstrate that the same PDA can be used for both the address of an account and CPI signing
use {
crate::state::MintConfig,
anchor_lang::prelude::*,
anchor_spl::{
metadata::{
create_metadata_accounts_v3, mpl_token_metadata::types::DataV2,
CreateMetadataAccountsV3, Metadata,
create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3, Metadata,
},
token::{Mint, Token},
},
Expand Down Expand Up @@ -39,6 +39,17 @@ pub struct CreateToken<'info> {
)]
pub metadata_account: UncheckedAccount<'info>,

// Records who is allowed to mint, since the mint authority PDA itself signs
// unconditionally for whoever calls the mint instruction.
#[account(
init,
payer = payer,
space = MintConfig::LEN,
seeds = [b"mint_config"],
bump,
)]
pub mint_config: Account<'info, MintConfig>,

pub token_program: Program<'info, Token>,
pub token_metadata_program: Program<'info, Metadata>,
pub system_program: Program<'info, System>,
Expand Down Expand Up @@ -86,6 +97,8 @@ pub fn create_token(
None, // Collection details
)?;

ctx.accounts.mint_config.admin = ctx.accounts.payer.key();

msg!("Token created successfully.");

Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use {
crate::{errors::TokenMinterError, state::MintConfig},
anchor_lang::prelude::*,
anchor_spl::{
associated_token::AssociatedToken,
Expand All @@ -19,6 +20,14 @@ pub struct MintToken<'info> {
)]
pub mint_account: Account<'info, Mint>,

// Only the wallet recorded as admin at create_token time may mint.
#[account(
seeds = [b"mint_config"],
bump,
constraint = mint_config.admin == payer.key() @ TokenMinterError::Unauthorized,
)]
pub mint_config: Account<'info, MintConfig>,

// Create Associated Token Account, if needed
// This is the account that will hold the minted tokens
#[account(
Expand All @@ -37,10 +46,7 @@ pub struct MintToken<'info> {
pub fn mint_token(ctx: Context<MintToken>, amount: u64) -> Result<()> {
msg!("Minting token to associated token account...");
msg!("Mint: {}", &ctx.accounts.mint_account.key());
msg!(
"Token Address: {}",
&ctx.accounts.associated_token_account.key()
);
msg!("Token Address: {}", &ctx.accounts.associated_token_account.key());

// PDA signer seeds
let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[ctx.bumps.mint_account]]];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anchor_lang::prelude::*;
use instructions::*;
pub mod errors;
pub mod instructions;
pub mod state;

declare_id!("3LFrPHqwk5jMrmiz48BFj6NV2k4NjobgTe1jChzx3JGD");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use anchor_lang::prelude::*;

// Tracks who is allowed to mint from this program's single global mint. The
// mint's own authority is a PDA that signs unconditionally, so this account is
// the only thing gating who may trigger it.
#[account]
pub struct MintConfig {
pub admin: Pubkey,
}

impl MintConfig {
pub const LEN: usize = 8 + 32;
}
32 changes: 31 additions & 1 deletion tokens/pda-mint-authority/anchor/tests/litesvm.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import * as anchor from '@anchor-lang/core';
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
import { PublicKey } from '@solana/web3.js';
import { Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
import { assert } from 'chai';
import { LiteSVMProvider } from 'anchor-litesvm';
import { LiteSVM } from 'litesvm';
import IDL from '../target/idl/token_minter.json';
import type { TokenMinter } from '../target/types/token_minter';

const expectAnchorError = async (promise: Promise<unknown>, code: string) => {
let caught: any;
try {
await promise;
} catch (error) {
caught = error;
}
assert.isDefined(caught, `expected the transaction to fail with ${code}`);
assert.strictEqual(caught?.error?.errorCode?.code, code, `expected ${code}, got: ${caught}`);
};

const PROGRAM_ID = new PublicKey(IDL.address);
const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');

Expand Down Expand Up @@ -60,4 +72,22 @@ describe('NFT Minter', () => {
console.log(` Associated Token Account Address: ${associatedTokenAccountAddress}`);
console.log(` Transaction Signature: ${transactionSignature}`);
});

it('rejects mint_token from a wallet that did not create the token', async () => {
const outsider = Keypair.generate();
client.airdrop(outsider.publicKey, BigInt(LAMPORTS_PER_SOL));
const outsiderAta = getAssociatedTokenAddressSync(mintPDA, outsider.publicKey);

await expectAnchorError(
program.methods
.mintToken(new anchor.BN(100))
.accountsPartial({
payer: outsider.publicKey,
associatedTokenAccount: outsiderAta,
})
.signers([outsider])
.rpc(),
'Unauthorized',
);
});
});
Loading