Skip to content

Commit

Permalink
Fixing collections to be burnable if empty.
Browse files Browse the repository at this point in the history
  • Loading branch information
blockiosaurus committed Jul 9, 2024
1 parent 742a3b5 commit 27e7093
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 23 deletions.
21 changes: 1 addition & 20 deletions clients/js/test/burn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
130 changes: 130 additions & 0 deletions clients/js/test/burnCollection.test.ts
Original file line number Diff line number Diff line change
@@ -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' });
});
11 changes: 8 additions & 3 deletions programs/mpl-core/src/state/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<ValidationResult, ProgramError> {
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.
Expand Down

0 comments on commit 27e7093

Please sign in to comment.