diff --git a/clients/js/test/close.test.ts b/clients/js/test/close.test.ts index 09f3bed..5b636ba 100644 --- a/clients/js/test/close.test.ts +++ b/clients/js/test/close.test.ts @@ -4,7 +4,9 @@ import { appendTransactionMessageInstructions, generateKeyPairSigner, getUtf8Encoder, + isSolanaError, pipe, + SOLANA_ERROR__INSTRUCTION_ERROR__INCORRECT_AUTHORITY, } from '@solana/kit'; import test from 'ava'; import { fetchMaybeMetadata, getCloseInstruction, getSetAuthorityInstruction } from '../src'; @@ -270,3 +272,86 @@ test('it can close keypair buffers', async t => { const account = await fetchMaybeMetadata(client.rpc, buffer.address); t.false(account.exists); }); + +test('it cannot close a keypair buffer with a different authority set using the buffer keypair', async t => { + // Given the following payer. + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + + // And the following pre-allocated keypair buffer. + const buffer = await createKeypairBuffer(client, { + payer, + data: getUtf8Encoder().encode('Hello, World!'), + }); + + // And we set a different authority on the buffer account. + const bufferAuthority = await generateKeyPairSigner(); + const setAuthorityIx = getSetAuthorityInstruction({ + account: buffer.address, + authority: buffer, + newAuthority: bufferAuthority.address, + }); + await pipe( + await createDefaultTransaction(client, payer), + tx => appendTransactionMessageInstruction(setAuthorityIx, tx), + tx => signAndSendTransaction(client, tx), + ); + + // When the buffer keypair tries to close its own account. + const closeIx = getCloseInstruction({ + account: buffer.address, + authority: buffer, + destination: payer.address, + }); + const promise = pipe( + await createDefaultTransaction(client, payer), + tx => appendTransactionMessageInstruction(closeIx, tx), + tx => signAndSendTransaction(client, tx), + ); + + // Then we expect a program error. + const error = await t.throwsAsync(promise); + t.true(isSolanaError(error)); + t.true(isSolanaError(error.cause, SOLANA_ERROR__INSTRUCTION_ERROR__INCORRECT_AUTHORITY)); +}); + +test('it can close a keypair buffer with its authority', async t => { + // Given the following payer. + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + + // And the following pre-allocated keypair buffer. + const buffer = await createKeypairBuffer(client, { + payer, + data: getUtf8Encoder().encode('Hello, World!'), + }); + + // And we set a different authority on the buffer account. + const bufferAuthority = await generateKeyPairSigner(); + const setAuthorityIx = getSetAuthorityInstruction({ + account: buffer.address, + authority: buffer, + newAuthority: bufferAuthority.address, + }); + await pipe( + await createDefaultTransaction(client, payer), + tx => appendTransactionMessageInstruction(setAuthorityIx, tx), + tx => signAndSendTransaction(client, tx), + ); + + // When the authority closes the buffer account. + const closeIx = getCloseInstruction({ + account: buffer.address, + authority: bufferAuthority, + destination: payer.address, + }); + await pipe( + await createDefaultTransaction(client, payer), + tx => appendTransactionMessageInstruction(closeIx, tx), + tx => signAndSendTransaction(client, tx), + ); + + // Then we expect the buffer account to no longer exist. + const account = await fetchMaybeMetadata(client.rpc, buffer.address); + t.false(account.exists); +}); diff --git a/program/src/processor/close.rs b/program/src/processor/close.rs index ebc3678..d8f2ccc 100644 --- a/program/src/processor/close.rs +++ b/program/src/processor/close.rs @@ -27,7 +27,7 @@ pub fn close(accounts: &[AccountInfo]) -> ProgramResult { // account // - must have data - // - authority must match (if not a keypair buffer) + // - authority must match let account_data = if account.data_is_empty() { return Err(ProgramError::UninitializedAccount); @@ -35,20 +35,16 @@ pub fn close(accounts: &[AccountInfo]) -> ProgramResult { unsafe { account.borrow_data_unchecked() } }; - // We only need to validate the authority if it is not a keypair buffer, - // since we already validated that the authority is a signer. - if account.key() != authority.key() { - match AccountDiscriminator::try_from(account_data[0])? { - AccountDiscriminator::Buffer => { - let buffer = unsafe { Buffer::from_bytes_unchecked(account_data) }; - validate_authority(buffer, authority, program, program_data)? - } - AccountDiscriminator::Metadata => { - let header = validate_metadata(account)?; - validate_authority(header, authority, program, program_data)? - } - _ => return Err(ProgramError::InvalidAccountData), + match AccountDiscriminator::try_from(account_data[0])? { + AccountDiscriminator::Buffer => { + let buffer = unsafe { Buffer::from_bytes_unchecked(account_data) }; + validate_authority(buffer, authority, program, program_data)? } + AccountDiscriminator::Metadata => { + let header = validate_metadata(account)?; + validate_authority(header, authority, program, program_data)? + } + _ => return Err(ProgramError::InvalidAccountData), } // Move the lamports to the destination account and close the account.