diff --git a/clients/js/test/setAuthority.test.ts b/clients/js/test/setAuthority.test.ts index bd1a37f..dee502f 100644 --- a/clients/js/test/setAuthority.test.ts +++ b/clients/js/test/setAuthority.test.ts @@ -17,6 +17,9 @@ import { fetchMetadata, getAllocateInstruction, getSetAuthorityInstruction, + getSetImmutableInstruction, + isProgramMetadataError, + PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT, } from '../src'; import { createCanonicalMetadata, @@ -392,3 +395,58 @@ test('the authority cannot remove itself on buffer accounts', async t => { const error = await t.throwsAsync(promise); t.true(isSolanaError(error.cause, SOLANA_ERROR__INSTRUCTION_ERROR__INVALID_ARGUMENT)); }); + +test('the authority cannot be changed on immutable metadata accounts', async t => { + // Given the following authorities and deployed program. + const client = createDefaultSolanaClient(); + const [authority, explicitAuthority, anotherAuthority] = await Promise.all([ + generateKeyPairSignerWithSol(client), + generateKeyPairSigner(), + generateKeyPairSigner(), + ]); + const [program, programData] = await createDeployedProgram(client, authority); + + // And the following initialized canonical metadata account. + const [metadata] = await createCanonicalMetadata(client, { + authority, + program, + programData, + seed: 'dummy', + data: getUtf8Encoder().encode('Hello, World!'), + }); + + // And given the explicit authority is set on the metadata account. + const setAuthorityIx = getSetAuthorityInstruction({ + account: metadata, + authority, + program, + programData, + newAuthority: explicitAuthority.address, + }); + + // And the explicit authority sets the metadata account to be immutable. + const setImmutableIx = getSetImmutableInstruction({ + metadata, + authority: explicitAuthority, + program, + programData, + }); + + // When the explicit authority attempts to set another authority on the + // metadata account after setting it to be immutable. + const setAnotherAuthorityIx = getSetAuthorityInstruction({ + account: metadata, + authority, + program, + programData, + newAuthority: anotherAuthority.address, + }); + const transactionMessage = pipe(await createDefaultTransaction(client, authority), tx => + appendTransactionMessageInstructions([setAuthorityIx, setImmutableIx, setAnotherAuthorityIx], tx), + ); + const promise = signAndSendTransaction(client, transactionMessage); + + // Then we expect the transaction to fail. + const error = await t.throwsAsync(promise); + t.true(isProgramMetadataError(error.cause, transactionMessage, PROGRAM_METADATA_ERROR__IMMUTABLE_METADATA_ACCOUNT)); +}); diff --git a/program/src/processor/set_authority.rs b/program/src/processor/set_authority.rs index db223e6..1c64387 100644 --- a/program/src/processor/set_authority.rs +++ b/program/src/processor/set_authority.rs @@ -1,6 +1,7 @@ use pinocchio::{account::AccountView, error::ProgramError, Address, ProgramResult}; use crate::{ + error::ProgramMetadataError, processor::validate_authority, state::{buffer::Buffer, header::Header, AccountDiscriminator, Zeroable}, }; @@ -61,6 +62,10 @@ pub fn set_authority(accounts: &mut [AccountView], instruction_data: &[u8]) -> P return Err(ProgramError::InvalidAccountData); } + if !header.mutable() { + return Err(ProgramMetadataError::ImmutableMetadataAccount.into()); + } + validate_authority(header, authority, program, program_data)?; header.authority = if *has_new_authority == 0 {