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 @@ -5,6 +5,7 @@ use {
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
Expand All @@ -14,7 +15,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 @@ -29,6 +30,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 metadata_account = next_account_info(accounts_iter)?;
let mint_config = next_account_info(accounts_iter)?;
let payer = next_account_info(accounts_iter)?;
let rent = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
Expand Down Expand Up @@ -97,6 +99,28 @@ pub fn create_token(program_id: &Pubkey, accounts: &[AccountInfo], args: CreateT
&[&[MintAuthorityPda::SEED_PREFIX.as_bytes(), &[bump]]],
)?;

// Record the creator so only they can later mint this NFT
//
msg!("Creating mint config account...");
msg!("Mint config address: {}", mint_config.key);
let (mint_config_pda, mint_config_bump) =
Pubkey::find_program_address(&[MintConfig::SEED_PREFIX.as_bytes(), mint_account.key.as_ref()], program_id);
if mint_config.key != &mint_config_pda {
return Err(ProgramError::InvalidSeeds);
}
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]]],
)?;
MintConfig { bump: mint_config_bump, admin: *payer.key }.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 @@ -31,6 +34,21 @@ pub fn mint_to(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
Pubkey::find_program_address(&[MintAuthorityPda::SEED_PREFIX.as_bytes()], program_id);
assert!(&mint_authority_pda.eq(mint_authority.key));

// Only the wallet recorded at create time may mint this NFT
//
let (mint_config_pda, _) =
Pubkey::find_program_address(&[MintConfig::SEED_PREFIX.as_bytes(), mint_account.key.as_ref()], program_id);
if mint_config.key != &mint_config_pda || mint_config.owner != program_id {
return Err(ProgramError::InvalidSeeds);
}
if !payer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let config = MintConfig::try_from_slice(&mint_config.data.borrow())?;
if config.admin != *payer.key {
return Err(ProgramError::IncorrectAuthority);
}

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

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

// Records who created a given mint, since the mint authority PDA signs
// unconditionally for whoever calls the mint instruction.
#[derive(BorshDeserialize, BorshSerialize)]
pub struct MintConfig {
pub bump: u8,
pub admin: Pubkey,
}

impl MintConfig {
pub const SEED_PREFIX: &'static str = "mint_config";
pub const SIZE: usize = 1 + 32;
}
62 changes: 59 additions & 3 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,11 @@ describe('NFT Minter', () => {

mintKeypair = await generateKeyPairSigner();

[mintConfigAddress] = await getProgramDerivedAddress({
programAddress: programId,
seeds: ['mint_config', addressEncoder.encode(mintKeypair.address)],
});

[metadataAddress] = await getProgramDerivedAddress({
programAddress: TOKEN_METADATA_PROGRAM_ADDRESS,
seeds: [
Expand All @@ -70,15 +76,19 @@ describe('NFT Minter', () => {
});
});

async function sendTransaction(ix: Instruction) {
async function trySendTransaction(ix: Instruction, feePayer: KeyPairSigner = payer) {
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
m => setTransactionMessageFeePayerSigner(payer, m),
m => setTransactionMessageFeePayerSigner(feePayer, m),
m => svm.setTransactionMessageLifetimeUsingLatestBlockhash(m),
m => appendTransactionMessageInstruction(ix, m),
);
const signedTx = await signTransactionMessageWithSigners(transactionMessage);
const result = svm.sendTransaction(signedTx);
return svm.sendTransaction(signedTx);
}

async function sendTransaction(ix: Instruction, feePayer: KeyPairSigner = payer) {
const result = await trySendTransaction(ix, feePayer);
assert(!(result instanceof FailedTransactionMetadata), `transaction failed: ${result.toString()}`);
}

Expand All @@ -98,6 +108,7 @@ describe('NFT Minter', () => {
mintKeypair,
mintAuthorityAddress,
metadataAddress,
mintConfigAddress,
payer,
programId,
'Homer NFT',
Expand All @@ -118,6 +129,46 @@ 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 account not created');
assert(mintConfigInfo.programAddress === programId, 'mint config account not owned by the program');
assert.deepEqual(
Array.from(mintConfigInfo.data.slice(1, 33)),
Array.from(addressEncoder.encode(payer.address)),
'creator not recorded in the mint config',
);
});

it('Rejects a Mint from a wallet that did not create the NFT', async () => {
const outsider = await generateKeyPairSigner();
svm.airdrop(outsider.address, lamports(10_000_000_000n));

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

const ix = createMintInstruction(
mintKeypair.address,
metadataAddress,
editionAddress,
mintAuthorityAddress,
mintConfigAddress,
outsiderTokenAccountAddress,
outsider,
programId,
);

const result = await trySendTransaction(ix, outsider);
assert(result instanceof FailedTransactionMetadata, 'an unrelated wallet minted the NFT created by the payer');

assert(!svm.getAccount(outsiderTokenAccountAddress).exists, 'outsider received a token account');
const mintInfo = svm.getAccount(mintKeypair.address);
assert(mintInfo.exists, 'mint account not found');
assert.equal(getMintDecoder().decode(mintInfo.data).supply, 0n, 'NFT was minted by an unrelated wallet');
assert(!svm.getAccount(editionAddress).exists, 'edition account was created by an unrelated wallet');
});

it('Mint the NFT to your wallet!', async () => {
Expand All @@ -132,6 +183,7 @@ describe('NFT Minter', () => {
metadataAddress,
editionAddress,
mintAuthorityAddress,
mintConfigAddress,
associatedTokenAccountAddress,
payer,
programId,
Expand All @@ -143,6 +195,10 @@ describe('NFT Minter', () => {
assert(tokenInfo.exists, 'associated token account not created');
const tokenAccount = getTokenDecoder().decode(tokenInfo.data);
assert.equal(tokenAccount.amount.toString(), '1', 'unexpected NFT balance');
assert(tokenAccount.owner === payer.address, 'NFT did not land in the creator wallet');
const mintInfo = svm.getAccount(mintKeypair.address);
assert(mintInfo.exists, 'mint account not found');
assert.equal(getMintDecoder().decode(mintInfo.data).supply, 1n, 'unexpected NFT supply');

const editionInfo = svm.getAccount(editionAddress);
assert(editionInfo.exists, 'edition account not created');
Expand Down
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 @@ -25,6 +25,7 @@ export function createCreateInstruction(
mint: TransactionSigner,
mintAuthority: Address,
metadata: Address,
mintConfig: Address,
payer: TransactionSigner,
programId: Address,
nftTitle: string,
Expand All @@ -37,6 +38,7 @@ export function createCreateInstruction(
{ address: mint.address, role: AccountRole.WRITABLE_SIGNER, signer: mint },
{ address: mintAuthority, role: AccountRole.WRITABLE },
{ address: metadata, role: AccountRole.WRITABLE },
{ address: mintConfig, role: AccountRole.WRITABLE },
{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer },
{ address: SYSVAR_RENT_ADDRESS, role: AccountRole.READONLY },
{ address: SYSTEM_PROGRAM_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 @@ -26,10 +26,11 @@ const CREATE_METADATA_ACCOUNT_V3: u8 = 33;
/// 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
/// 3. `[writable]` mint config PDA (records the creator; created here)
/// 4. `[signer, writable]` payer (funds the new accounts and becomes the creator)
/// 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, metadata_account, mint_config, payer, system_program, _token_program, _token_metadata_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
Expand Down Expand Up @@ -101,6 +102,29 @@ pub fn create_token(program_id: &Address, accounts: &mut [AccountView], data: &[
&signers,
)?;

// Record the creator so only they can later mint this NFT.
log!("Creating mint config account");
let (mint_config_pda, mint_config_bump) =
Address::find_program_address(&[MintConfig::SEED_PREFIX, mint_account.address().as_ref()], program_id);
if mint_config.address() != &mint_config_pda {
return Err(ProgramError::InvalidSeeds);
}
let mint_config_bump_bytes = [mint_config_bump];
let mint_config_seeds = [
Seed::from(MintConfig::SEED_PREFIX),
Seed::from(mint_account.address().as_ref()),
Seed::from(&mint_config_bump_bytes),
];
CreateAccount {
from: payer,
to: mint_config,
lamports: rent.try_minimum_balance(MintConfig::ACCOUNT_SPACE)?,
space: MintConfig::ACCOUNT_SPACE as u64,
owner: program_id,
}
.invoke_signed(&[Signer::from(&mint_config_seeds)])?;
MintConfig { bump: mint_config_bump, admin: *payer.address() }.serialize(&mut mint_config.try_borrow_mut()?)?;

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