Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing collections to be burnable if empty. #162

Merged
merged 3 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 {
danenbm marked this conversation as resolved.
Show resolved Hide resolved
approve!()
} else {
abstain!()
}
}

/// Validate the update lifecycle event.
Expand Down
Loading