Skip to content

Commit

Permalink
Prevent thawing pNFT editions (#112)
Browse files Browse the repository at this point in the history
* Add public sync script

* Fixing the the assertion that prevents thawing pNFT editions.

* Fixing formatting

* Updating comments and parameter name.

* Remove unneeded file

---------

Co-authored-by: febo <[email protected]>
Co-authored-by: blockiosaurus <[email protected]>
Co-authored-by: blockiosaurus <[email protected]>
  • Loading branch information
4 people authored Mar 28, 2024
1 parent 609afd2 commit 87af917
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 6 deletions.
66 changes: 66 additions & 0 deletions clients/js/test/printV1.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
createMintWithAssociatedToken,
findAssociatedTokenPda,
setComputeUnitLimit,
} from '@metaplex-foundation/mpl-toolbox';
import {
generateSigner,
percentAmount,
publicKey,
some,
transactionBuilder,
} from '@metaplex-foundation/umi';
Expand All @@ -13,11 +15,14 @@ import {
DigitalAsset,
DigitalAssetWithToken,
TokenStandard,
delegateSaleV1,
fetchDigitalAsset,
fetchDigitalAssetWithAssociatedToken,
findMasterEditionPda,
findTokenRecordPda,
printSupply,
printV1,
thawDelegatedAccount,
} from '../src';
import { createDigitalAssetWithToken, createUmi } from './_setup';

Expand Down Expand Up @@ -252,3 +257,64 @@ test('it cannot print a new edition if the initialized edition mint account has
// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidMintForTokenStandard' });
});

test('it cannot thaw the token on a pNFT Edition', async (t) => {
// Given an existing master edition PNFT.
const umi = await createUmi();
const originalOwner = generateSigner(umi);
const originalMint = await createDigitalAssetWithToken(umi, {
name: 'My PNFT',
symbol: 'MPNFT',
uri: 'https://example.com/pnft.json',
sellerFeeBasisPoints: percentAmount(5.42),
tokenOwner: originalOwner.publicKey,
printSupply: printSupply('Limited', [10]),
tokenStandard: TokenStandard.ProgrammableNonFungible,
});
const saleDelegate = generateSigner(umi);

// When we print a new edition of the asset.
const editionMint = generateSigner(umi);
const editionOwner = generateSigner(umi);
const editionTokenAccount = findAssociatedTokenPda(umi, {
mint: editionMint.publicKey,
owner: editionOwner.publicKey,
});
const editionTokenRecord = findTokenRecordPda(umi, {
mint: editionMint.publicKey,
token: publicKey(editionTokenAccount),
});
await transactionBuilder()
.add(setComputeUnitLimit(umi, { units: 400_000 }))
.add(
printV1(umi, {
masterTokenAccountOwner: originalOwner,
masterEditionMint: originalMint.publicKey,
editionMint,
editionTokenAccountOwner: editionOwner.publicKey,
editionNumber: 1,
tokenStandard: TokenStandard.ProgrammableNonFungible,
})
)
.add(
delegateSaleV1(umi, {
delegate: saleDelegate.publicKey,
mint: editionMint.publicKey,
tokenOwner: editionOwner.publicKey,
authority: editionOwner,
tokenStandard: TokenStandard.ProgrammableNonFungibleEdition,
tokenRecord: editionTokenRecord,
})
)
.sendAndConfirm(umi);

// Try to thaw the token.
const result = thawDelegatedAccount(umi, {
delegate: saleDelegate,
tokenAccount: editionTokenAccount,
mint: editionMint.publicKey,
}).sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(result, { name: 'InvalidTokenStandard' });
});
66 changes: 66 additions & 0 deletions clients/js/test/printV2.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {
createMintWithAssociatedToken,
findAssociatedTokenPda,
setComputeUnitLimit,
} from '@metaplex-foundation/mpl-toolbox';
import {
generateSigner,
percentAmount,
publicKey,
sol,
some,
transactionBuilder,
Expand All @@ -15,12 +17,15 @@ import {
DigitalAssetWithToken,
TokenStandard,
delegatePrintDelegateV1,
delegateSaleV1,
fetchDigitalAsset,
fetchDigitalAssetWithAssociatedToken,
findHolderDelegateRecordPda,
findMasterEditionPda,
findTokenRecordPda,
printSupply,
printV2,
thawDelegatedAccount,
} from '../src';
import { createDigitalAssetWithToken, createUmi } from './_setup';

Expand Down Expand Up @@ -657,3 +662,64 @@ test('it can delegate the authority to print a new edition with a separate payer
},
});
});

test('it cannot thaw the token on a pNFT Edition', async (t) => {
// Given an existing master edition PNFT.
const umi = await createUmi();
const originalOwner = generateSigner(umi);
const originalMint = await createDigitalAssetWithToken(umi, {
name: 'My PNFT',
symbol: 'MPNFT',
uri: 'https://example.com/pnft.json',
sellerFeeBasisPoints: percentAmount(5.42),
tokenOwner: originalOwner.publicKey,
printSupply: printSupply('Limited', [10]),
tokenStandard: TokenStandard.ProgrammableNonFungible,
});
const saleDelegate = generateSigner(umi);

// When we print a new edition of the asset.
const editionMint = generateSigner(umi);
const editionOwner = generateSigner(umi);
const editionTokenAccount = findAssociatedTokenPda(umi, {
mint: editionMint.publicKey,
owner: editionOwner.publicKey,
});
const editionTokenRecord = findTokenRecordPda(umi, {
mint: editionMint.publicKey,
token: publicKey(editionTokenAccount),
});
await transactionBuilder()
.add(setComputeUnitLimit(umi, { units: 400_000 }))
.add(
printV2(umi, {
masterTokenAccountOwner: originalOwner,
masterEditionMint: originalMint.publicKey,
editionMint,
editionTokenAccountOwner: editionOwner.publicKey,
editionNumber: 1,
tokenStandard: TokenStandard.ProgrammableNonFungible,
})
)
.add(
delegateSaleV1(umi, {
delegate: saleDelegate.publicKey,
mint: editionMint.publicKey,
tokenOwner: editionOwner.publicKey,
authority: editionOwner,
tokenStandard: TokenStandard.ProgrammableNonFungibleEdition,
tokenRecord: editionTokenRecord,
})
)
.sendAndConfirm(umi);

// Try to thaw the token.
const result = thawDelegatedAccount(umi, {
delegate: saleDelegate,
tokenAccount: editionTokenAccount,
mint: editionMint.publicKey,
}).sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(result, { name: 'InvalidTokenStandard' });
});
21 changes: 15 additions & 6 deletions programs/token-metadata/program/src/assertions/edition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use spl_token_2022::state::Mint;
use crate::{
error::MetadataError,
pda::find_master_edition_account,
state::{TokenStandard, EDITION, PREFIX, TOKEN_STANDARD_INDEX},
state::{
Key, TokenStandard, EDITION, PREFIX, TOKEN_STANDARD_INDEX, TOKEN_STANDARD_INDEX_EDITION,
},
utils::unpack,
};

Expand All @@ -22,12 +24,19 @@ pub fn assert_edition_is_not_mint_authority(mint_account_info: &AccountInfo) ->
Ok(())
}

/// Checks that the `master_edition` is not a pNFT master edition.
pub fn assert_edition_is_not_programmable(master_edition_info: &AccountInfo) -> ProgramResult {
let edition_data = master_edition_info.data.borrow();
/// Checks that the `edition` is not a pNFT master edition or edition.
pub fn assert_edition_is_not_programmable(edition_info: &AccountInfo) -> ProgramResult {
let edition_data = edition_info.data.borrow();

if edition_data.len() > TOKEN_STANDARD_INDEX
&& edition_data[TOKEN_STANDARD_INDEX] == TokenStandard::ProgrammableNonFungible as u8
// Check if it's a master edition of a pNFT
if (edition_data.len() > TOKEN_STANDARD_INDEX
&& edition_data[0] == Key::MasterEditionV2 as u8
// Check if it's an edition of a pNFT
&& (edition_data[TOKEN_STANDARD_INDEX] == TokenStandard::ProgrammableNonFungible as u8))
|| (edition_data.len() > TOKEN_STANDARD_INDEX_EDITION
&& edition_data[0] == Key::EditionV1 as u8
&& edition_data[TOKEN_STANDARD_INDEX_EDITION]
== TokenStandard::ProgrammableNonFungible as u8)
{
return Err(MetadataError::InvalidTokenStandard.into());
}
Expand Down

0 comments on commit 87af917

Please sign in to comment.