diff --git a/clients/js/test/burn.test.ts b/clients/js/test/burn.test.ts index 89c4dd89..4b82aec8 100644 --- a/clients/js/test/burn.test.ts +++ b/clients/js/test/burn.test.ts @@ -2,7 +2,7 @@ import { generateSigner, sol } from '@metaplex-foundation/umi'; import test from 'ava'; import { generateSignerWithSol } from '@metaplex-foundation/umi-bundle-tests'; -import { burnCollectionV1, burnV1, pluginAuthorityPair } from '../src'; +import { burnV1, pluginAuthorityPair } from '../src'; import { DEFAULT_ASSET, DEFAULT_COLLECTION, @@ -208,25 +208,6 @@ test('it cannot use an invalid noop program for assets', async (t) => { await t.throwsAsync(result, { name: 'InvalidLogWrapperProgram' }); }); -test('it cannot use an invalid noop program for collections', async (t) => { - const umi = await createUmi(); - const collection = await createCollection(umi); - const fakeLogWrapper = generateSigner(umi); - await assertCollection(t, umi, { - ...DEFAULT_COLLECTION, - collection: collection.publicKey, - updateAuthority: umi.identity.publicKey, - }); - - const result = burnCollectionV1(umi, { - collection: collection.publicKey, - logWrapper: fakeLogWrapper.publicKey, - compressionProof: null, - }).sendAndConfirm(umi); - - await t.throwsAsync(result, { name: 'InvalidLogWrapperProgram' }); -}); - test('it can burn using owner authority', async (t) => { const umi = await createUmi(); const owner = await generateSignerWithSol(umi); diff --git a/clients/js/test/burnCollection.test.ts b/clients/js/test/burnCollection.test.ts new file mode 100644 index 00000000..a0cb7f2b --- /dev/null +++ b/clients/js/test/burnCollection.test.ts @@ -0,0 +1,130 @@ +import { generateSigner, sol } from '@metaplex-foundation/umi'; +import test from 'ava'; + +import { generateSignerWithSol } from '@metaplex-foundation/umi-bundle-tests'; +import { burnCollection } from '../src'; +import { + DEFAULT_COLLECTION, + assertBurned, + assertCollection, + createAssetWithCollection, + createCollection, + createUmi, +} from './_setupRaw'; + +test('it can burn a collection as the authority', async (t) => { + const umi = await createUmi(); + const collection = await createCollection(umi); + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + }); + + await burnCollection(umi, { + collection: collection.publicKey, + compressionProof: null, + }).sendAndConfirm(umi); + + // And the asset address still exists but was resized to 1. + const afterCollection = await assertBurned(t, umi, collection.publicKey); + t.deepEqual(afterCollection.lamports, sol(0.00089784)); +}); + +test('it cannot burn a collection if not the authority', async (t) => { + const umi = await createUmi(); + const attacker = generateSigner(umi); + + const collection = await createCollection(umi); + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + }); + + const result = burnCollection(umi, { + collection: collection.publicKey, + authority: attacker, + compressionProof: null, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + }); +}); + +test('it cannot burn a collection if it has Assets in it', async (t) => { + const umi = await createUmi(); + + const { collection } = await createAssetWithCollection(umi, {}); + + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + }); + + const result = burnCollection(umi, { + collection: collection.publicKey, + compressionProof: null, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidAuthority' }); + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + }); +}); + +test('it can burn asset with different payer', async (t) => { + const umi = await createUmi(); + const authority = await generateSignerWithSol(umi); + const collection = await createCollection(umi, { + updateAuthority: authority.publicKey, + }); + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: authority.publicKey, + }); + + const lamportsBefore = await umi.rpc.getBalance(umi.identity.publicKey); + + await burnCollection(umi, { + collection: collection.publicKey, + payer: umi.identity, + authority, + compressionProof: null, + }).sendAndConfirm(umi); + + // And the asset address still exists but was resized to 1. + const afterCollection = await assertBurned(t, umi, collection.publicKey); + t.deepEqual(afterCollection.lamports, sol(0.00089784)); + + const lamportsAfter = await umi.rpc.getBalance(umi.identity.publicKey); + + t.true(lamportsAfter.basisPoints > lamportsBefore.basisPoints); +}); + +test('it cannot use an invalid noop program for collections', async (t) => { + const umi = await createUmi(); + const collection = await createCollection(umi); + const fakeLogWrapper = generateSigner(umi); + await assertCollection(t, umi, { + ...DEFAULT_COLLECTION, + collection: collection.publicKey, + updateAuthority: umi.identity.publicKey, + }); + + const result = burnCollection(umi, { + collection: collection.publicKey, + logWrapper: fakeLogWrapper.publicKey, + compressionProof: null, + }).sendAndConfirm(umi); + + await t.throwsAsync(result, { name: 'InvalidLogWrapperProgram' }); +}); diff --git a/programs/mpl-core/src/state/collection.rs b/programs/mpl-core/src/state/collection.rs index 1b4767fd..f014de27 100644 --- a/programs/mpl-core/src/state/collection.rs +++ b/programs/mpl-core/src/state/collection.rs @@ -80,7 +80,7 @@ impl CollectionV1 { /// Check permissions for the burn lifecycle event. pub fn check_burn() -> CheckResult { - CheckResult::None + CheckResult::CanApprove } /// Check permissions for the update lifecycle event. @@ -220,11 +220,16 @@ impl CollectionV1 { /// Validate the burn lifecycle event. pub fn validate_burn( &self, - _authority_info: &AccountInfo, + authority_info: &AccountInfo, _: Option<&Plugin>, _: Option<&ExternalPluginAdapter>, ) -> Result { - abstain!() + // If the update authority is the one burning the collection, and the collection is empty, then it can be burned. + if authority_info.key == &self.update_authority && self.current_size == 0 { + approve!() + } else { + abstain!() + } } /// Validate the update lifecycle event.