Skip to content
Open
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
Expand Up @@ -14,7 +14,7 @@ use {
spl_token_interface::{instruction as token_instruction, state::Mint},
};

use crate::state::MintAuthorityPda;
use crate::state::{MintAuthorityPda, MintConfig};

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CreateTokenArgs {
Expand All @@ -28,6 +28,7 @@ pub fn create_token(program_id: &Pubkey, accounts: &[AccountInfo], args: CreateT

let mint_account = next_account_info(accounts_iter)?;
let mint_authority = next_account_info(accounts_iter)?;
let mint_config = next_account_info(accounts_iter)?;
let metadata_account = next_account_info(accounts_iter)?;
let payer = next_account_info(accounts_iter)?;
let rent = next_account_info(accounts_iter)?;
Expand All @@ -39,6 +40,10 @@ pub fn create_token(program_id: &Pubkey, accounts: &[AccountInfo], args: CreateT
Pubkey::find_program_address(&[MintAuthorityPda::SEED_PREFIX.as_bytes()], program_id);
assert!(&mint_authority_pda.eq(mint_authority.key));

let (mint_config_pda, mint_config_bump) =
Pubkey::find_program_address(&[MintConfig::SEED_PREFIX.as_bytes(), mint_account.key.as_ref()], program_id);
assert!(&mint_config_pda.eq(mint_config.key));

// First create the account for the Mint
//
msg!("Creating mint account...");
Expand Down Expand Up @@ -97,6 +102,23 @@ pub fn create_token(program_id: &Pubkey, accounts: &[AccountInfo], args: CreateT
&[&[MintAuthorityPda::SEED_PREFIX.as_bytes(), &[bump]]],
)?;

// Records who is allowed to call mint_to, since the mint authority PDA itself
// signs unconditionally for whoever calls it.
msg!("Creating mint config account...");
invoke_signed(
&system_instruction::create_account(
payer.key,
mint_config.key,
(Rent::get()?).minimum_balance(MintConfig::SIZE),
MintConfig::SIZE as u64,
program_id,
),
&[mint_config.clone(), payer.clone(), system_program.clone()],
&[&[MintConfig::SEED_PREFIX.as_bytes(), mint_account.key.as_ref(), &[mint_config_bump]]],
)?;
let config = MintConfig { admin: *payer.key };
config.serialize(&mut &mut mint_config.data.borrow_mut()[..])?;

msg!("Token mint created successfully.");

Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use {
borsh::BorshDeserialize,
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
pubkey::Pubkey,
},
spl_associated_token_account_interface::instruction as associated_token_account_instruction,
spl_token_interface::instruction as token_instruction,
};

use crate::state::MintAuthorityPda;
use crate::state::{MintAuthorityPda, MintConfig};

pub fn mint_to(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
Expand All @@ -19,6 +21,7 @@ pub fn mint_to(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let metadata_account = next_account_info(accounts_iter)?;
let edition_account = next_account_info(accounts_iter)?;
let mint_authority = next_account_info(accounts_iter)?;
let mint_config = next_account_info(accounts_iter)?;
let associated_token_account = next_account_info(accounts_iter)?;
let payer = next_account_info(accounts_iter)?;
let _rent = next_account_info(accounts_iter)?;
Expand All @@ -27,10 +30,26 @@ pub fn mint_to(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let associated_token_program = next_account_info(accounts_iter)?;
let token_metadata_program = next_account_info(accounts_iter)?;

if !payer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}

let (mint_authority_pda, bump) =
Pubkey::find_program_address(&[MintAuthorityPda::SEED_PREFIX.as_bytes()], program_id);
assert!(&mint_authority_pda.eq(mint_authority.key));

let (mint_config_pda, _) =
Pubkey::find_program_address(&[MintConfig::SEED_PREFIX.as_bytes(), mint_account.key.as_ref()], program_id);
assert!(&mint_config_pda.eq(mint_config.key));
if mint_config.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Only the wallet recorded as admin at create_token time may mint.
if MintConfig::deserialize(&mut &mint_config.data.borrow()[..])?.admin != *payer.key {
msg!("Only the admin recorded at token creation may mint");
return Err(ProgramError::InvalidArgument);
}

if associated_token_account.lamports() == 0 {
msg!("Creating associated token account...");
invoke(
Expand Down
11 changes: 11 additions & 0 deletions tokens/pda-mint-authority/native/program/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::pubkey::Pubkey;

#[derive(BorshDeserialize, BorshSerialize)]
pub struct MintAuthorityPda {
Expand All @@ -9,3 +10,13 @@ impl MintAuthorityPda {
pub const SEED_PREFIX: &'static str = "mint_authority";
pub const SIZE: usize = 8 + 8;
}

#[derive(BorshDeserialize, BorshSerialize)]
pub struct MintConfig {
pub admin: Pubkey,
}

impl MintConfig {
pub const SEED_PREFIX: &'static str = "mint_config";
pub const SIZE: usize = 32;
}
84 changes: 84 additions & 0 deletions tokens/pda-mint-authority/native/tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('NFT Minter', () => {
let mintKeypair: KeyPairSigner;
let mintAuthorityAddress: Address;
let mintAuthorityBump: number;
let mintConfigAddress: Address;
let metadataAddress: Address;
let editionAddress: Address;

Expand All @@ -50,6 +51,12 @@ describe('NFT Minter', () => {

mintKeypair = await generateKeyPairSigner();

// The mint config PDA is bound to this mint's address.
[mintConfigAddress] = await getProgramDerivedAddress({
programAddress: programId,
seeds: ['mint_config', addressEncoder.encode(mintKeypair.address)],
});

[metadataAddress] = await getProgramDerivedAddress({
programAddress: TOKEN_METADATA_PROGRAM_ADDRESS,
seeds: [
Expand Down Expand Up @@ -97,6 +104,7 @@ describe('NFT Minter', () => {
const ix = createCreateInstruction(
mintKeypair,
mintAuthorityAddress,
mintConfigAddress,
metadataAddress,
payer,
programId,
Expand All @@ -118,6 +126,10 @@ describe('NFT Minter', () => {
const metadataInfo = svm.getAccount(metadataAddress);
assert(metadataInfo.exists, 'metadata account not created');
assert(metadataInfo.programAddress === TOKEN_METADATA_PROGRAM_ADDRESS, 'metadata account has wrong owner');

const mintConfigInfo = svm.getAccount(mintConfigAddress);
assert(mintConfigInfo.exists, 'mint config PDA not created');
assert(mintConfigInfo.programAddress === programId, 'mint config PDA not owned by the program');
});

it('Mint the NFT to your wallet!', async () => {
Expand All @@ -132,6 +144,7 @@ describe('NFT Minter', () => {
metadataAddress,
editionAddress,
mintAuthorityAddress,
mintConfigAddress,
associatedTokenAccountAddress,
payer,
programId,
Expand All @@ -148,4 +161,75 @@ describe('NFT Minter', () => {
assert(editionInfo.exists, 'edition account not created');
assert(editionInfo.programAddress === TOKEN_METADATA_PROGRAM_ADDRESS, 'edition account has wrong owner');
});

it('rejects mint from a wallet that did not create the token', async () => {
// A fresh mint is required: after the happy-path mint, Metaplex already holds
// the mint authority via the master edition, so a second mint would fail even
// without the admin check. This test must fail *only* because of that check.
const otherMint = await generateKeyPairSigner();
const [otherMintConfig] = await getProgramDerivedAddress({
programAddress: programId,
seeds: ['mint_config', addressEncoder.encode(otherMint.address)],
});
const [otherMetadata] = await getProgramDerivedAddress({
programAddress: TOKEN_METADATA_PROGRAM_ADDRESS,
seeds: [
'metadata',
addressEncoder.encode(TOKEN_METADATA_PROGRAM_ADDRESS),
addressEncoder.encode(otherMint.address),
],
});
const [otherEdition] = await getProgramDerivedAddress({
programAddress: TOKEN_METADATA_PROGRAM_ADDRESS,
seeds: [
'metadata',
addressEncoder.encode(TOKEN_METADATA_PROGRAM_ADDRESS),
addressEncoder.encode(otherMint.address),
'edition',
],
});

await sendTransaction(
createCreateInstruction(
otherMint,
mintAuthorityAddress,
otherMintConfig,
otherMetadata,
payer,
programId,
'Homer NFT',
'HOMR',
'https://raw.githubusercontent.com/solana-developers/program-examples/new-examples/tokens/tokens/.assets/nft.json',
),
);

const outsider = await generateKeyPairSigner();
svm.airdrop(outsider.address, lamports(10_000_000_000n));

const [outsiderAta] = await findAssociatedTokenPda({
mint: otherMint.address,
owner: outsider.address,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});

const ix = createMintInstruction(
otherMint.address,
otherMetadata,
otherEdition,
mintAuthorityAddress,
otherMintConfig,
outsiderAta,
outsider,
programId,
);
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayerSigner(outsider, m),
m => svm.setTransactionMessageLifetimeUsingLatestBlockhash(m),
m => appendTransactionMessageInstruction(ix, m),
);
const signedTx = await signTransactionMessageWithSigners(transactionMessage);
const result = svm.sendTransaction(signedTx);
assert(result instanceof FailedTransactionMetadata, 'expected the transaction to fail');
});
});
2 changes: 2 additions & 0 deletions tokens/pda-mint-authority/native/ts/instructions/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const createEncoder = getStructEncoder([
export function createCreateInstruction(
mint: TransactionSigner,
mintAuthority: Address,
mintConfig: Address,
metadata: Address,
payer: TransactionSigner,
programId: Address,
Expand All @@ -36,6 +37,7 @@ export function createCreateInstruction(
accounts: [
{ address: mint.address, role: AccountRole.WRITABLE_SIGNER, signer: mint },
{ address: mintAuthority, role: AccountRole.WRITABLE },
{ address: mintConfig, role: AccountRole.WRITABLE },
{ address: metadata, role: AccountRole.WRITABLE },
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
{ address: SYSVAR_RENT_ADDRESS, role: AccountRole.READONLY },
Expand Down
2 changes: 2 additions & 0 deletions tokens/pda-mint-authority/native/ts/instructions/mint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function createMintInstruction(
metadata: Address,
edition: Address,
mintAuthority: Address,
mintConfig: Address,
associatedTokenAccount: Address,
payer: TransactionSigner,
programId: Address,
Expand All @@ -23,6 +24,7 @@ export function createMintInstruction(
{ address: metadata, role: AccountRole.WRITABLE },
{ address: edition, role: AccountRole.WRITABLE },
{ address: mintAuthority, role: AccountRole.WRITABLE },
{ address: mintConfig, role: AccountRole.READONLY },
{ address: associatedTokenAccount, role: AccountRole.WRITABLE },
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
{ address: SYSVAR_RENT_ADDRESS, role: AccountRole.READONLY },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use pinocchio_system::instructions::CreateAccount;
use pinocchio_token::instructions::InitializeMint2;

use crate::instructions::{CreateTokenArgs, MINT_SIZE, TOKEN_DECIMALS, TOKEN_METADATA_PROGRAM_ID};
use crate::state::MintAuthorityPda;
use crate::state::{MintAuthorityPda, MintConfig};

/// Discriminator of the Metaplex `CreateMetadataAccountV3` instruction (variant
/// 33 of the Token Metadata program's instruction enum).
Expand All @@ -25,11 +25,12 @@ const CREATE_METADATA_ACCOUNT_V3: u8 = 33;
/// Accounts:
/// 0. `[signer, writable]` mint account (a fresh keypair to initialize)
/// 1. `[]` mint authority PDA (also the metadata update authority)
/// 2. `[writable]` metadata account (the Metaplex metadata PDA)
/// 3. `[signer, writable]` payer (funds the new accounts)
/// 4. `[]` system program
/// 5. `[]` token program
/// 6. `[]` token metadata program
/// 2. `[writable]` mint config PDA (created here; records the payer)
/// 3. `[writable]` metadata account (the Metaplex metadata PDA)
/// 4. `[signer, writable]` payer (funds the new accounts)
/// 5. `[]` system program
/// 6. `[]` token program
/// 7. `[]` token metadata program
///
/// Instruction data: Borsh `[name: string, symbol: string, uri: string]`.
///
Expand All @@ -39,7 +40,7 @@ const CREATE_METADATA_ACCOUNT_V3: u8 = 33;
pub fn create_token(program_id: &Address, accounts: &mut [AccountView], data: &[u8]) -> ProgramResult {
// `token_program` and `token_metadata_program` are unused directly, but must
// be supplied so they are present in the transaction for the CPIs below.
let [mint_account, mint_authority, metadata_account, payer, system_program, _token_program, _token_metadata_program] =
let [mint_account, mint_authority, mint_config, metadata_account, payer, system_program, _token_program, _token_metadata_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
Expand All @@ -58,6 +59,13 @@ pub fn create_token(program_id: &Address, accounts: &mut [AccountView], data: &[
return Err(ProgramError::InvalidSeeds);
}

// Confirm the supplied account is the mint-config PDA bound to this mint.
let (mint_config_pda, mint_config_bump) =
Address::find_program_address(&[MintConfig::SEED_PREFIX, mint_account.address().as_array()], program_id);
if mint_config.address() != &mint_config_pda {
return Err(ProgramError::InvalidSeeds);
}

// Fund the mint account with enough lamports to stay rent-exempt, read from
// the Rent sysvar.
let rent = Rent::get()?;
Expand Down Expand Up @@ -101,6 +109,23 @@ pub fn create_token(program_id: &Address, accounts: &mut [AccountView], data: &[
&signers,
)?;

// Records who is allowed to call `mint_to`, since the mint authority PDA
// itself signs unconditionally for whoever calls it.
log!("Creating mint config account");
let config_lamports = rent.try_minimum_balance(MintConfig::ACCOUNT_SPACE)?;
let bump_bytes = [mint_config_bump];
let seeds =
[Seed::from(MintConfig::SEED_PREFIX), Seed::from(mint_account.address().as_ref()), Seed::from(&bump_bytes)];
CreateAccount {
from: payer,
to: mint_config,
lamports: config_lamports,
space: MintConfig::ACCOUNT_SPACE as u64,
owner: program_id,
}
.invoke_signed(&[Signer::from(&seeds)])?;
MintConfig { admin: *payer.address() }.serialize(&mut mint_config.try_borrow_mut()?)?;

log!("Token mint created successfully");
Ok(())
}
Expand Down
Loading