From f479bd23951817ce3932a5557b605d80f7b7888e Mon Sep 17 00:00:00 2001 From: Swinecoder101 Date: Wed, 26 Aug 2026 12:50:52 +0100 Subject: [PATCH 1/2] test(transfer-tokens): prove native transfer fails when the recipient does not sign --- tokens/transfer-tokens/native/tests/test.ts | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tokens/transfer-tokens/native/tests/test.ts b/tokens/transfer-tokens/native/tests/test.ts index de2704d44..286e4f4ee 100644 --- a/tokens/transfer-tokens/native/tests/test.ts +++ b/tokens/transfer-tokens/native/tests/test.ts @@ -1,4 +1,5 @@ import { + AccountRole, type Address, appendTransactionMessageInstruction, createTransactionMessage, @@ -242,4 +243,27 @@ describe('Transferring Tokens', () => { assert.equal(tokenBalance(toAta), '1', 'unexpected recipient NFT balance'); assert.equal(tokenBalance(fromAta), '0', 'unexpected sender NFT balance'); }); + + it('Transfer tokens to a wallet that does not sign the transaction!', async () => { + const mint = tokenMintKeypair.address; + const nonSigningRecipient = (await generateKeyPairSigner()).address; + const fromAta = await findAssociatedTokenAddress(mint, payer.address); + const toAta = await findAssociatedTokenAddress(mint, nonSigningRecipient); + const senderBalanceBefore = BigInt(tokenBalance(fromAta)); + const quantity = 20n; + + // The client builder forces the recipient to sign; a real recipient never does. + const built = createTransferTokensInstruction(mint, fromAta, toAta, payer, payer, payer, programId, quantity); + const ix: Instruction = { + ...built, + accounts: built.accounts.map((meta, i) => + i === 4 ? { address: nonSigningRecipient, role: AccountRole.READONLY } : meta, + ), + }; + + await sendTransaction(ix); + + assert.equal(tokenBalance(toAta), quantity.toString(), 'unexpected recipient balance'); + assert.equal(tokenBalance(fromAta), (senderBalanceBefore - quantity).toString(), 'unexpected sender balance'); + }); }); From 9554821d035443c9244befa2c9dd8e6b18f25c7a Mon Sep 17 00:00:00 2001 From: Swinecoder101 Date: Wed, 26 Aug 2026 12:52:14 +0100 Subject: [PATCH 2/2] fix(transfer-tokens): stop requiring the recipient to sign native token transfers --- .../program/src/instructions/transfer.rs | 4 +--- tokens/transfer-tokens/native/tests/test.ts | 21 ++++++++++--------- .../native/ts/instructions/transfer.ts | 4 ++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/tokens/transfer-tokens/native/program/src/instructions/transfer.rs b/tokens/transfer-tokens/native/program/src/instructions/transfer.rs index 9d265b941..7519a60b3 100644 --- a/tokens/transfer-tokens/native/program/src/instructions/transfer.rs +++ b/tokens/transfer-tokens/native/program/src/instructions/transfer.rs @@ -62,15 +62,13 @@ pub fn transfer_tokens(accounts: &[AccountInfo], args: TransferTokensArgs) -> Pr from_associated_token_account.key, to_associated_token_account.key, owner.key, - &[owner.key, recipient.key], + &[owner.key], args.quantity, )?, &[ - mint_account.clone(), from_associated_token_account.clone(), to_associated_token_account.clone(), owner.clone(), - recipient.clone(), token_program.clone(), ], )?; diff --git a/tokens/transfer-tokens/native/tests/test.ts b/tokens/transfer-tokens/native/tests/test.ts index 286e4f4ee..6e20e09e1 100644 --- a/tokens/transfer-tokens/native/tests/test.ts +++ b/tokens/transfer-tokens/native/tests/test.ts @@ -1,5 +1,4 @@ import { - AccountRole, type Address, appendTransactionMessageInstruction, createTransactionMessage, @@ -217,7 +216,7 @@ describe('Transferring Tokens', () => { await findAssociatedTokenAddress(mint, payer.address), await findAssociatedTokenAddress(mint, recipientWallet.address), payer, - recipientWallet, + recipientWallet.address, payer, programId, quantity, @@ -252,14 +251,16 @@ describe('Transferring Tokens', () => { const senderBalanceBefore = BigInt(tokenBalance(fromAta)); const quantity = 20n; - // The client builder forces the recipient to sign; a real recipient never does. - const built = createTransferTokensInstruction(mint, fromAta, toAta, payer, payer, payer, programId, quantity); - const ix: Instruction = { - ...built, - accounts: built.accounts.map((meta, i) => - i === 4 ? { address: nonSigningRecipient, role: AccountRole.READONLY } : meta, - ), - }; + const ix = createTransferTokensInstruction( + mint, + fromAta, + toAta, + payer, + nonSigningRecipient, + payer, + programId, + quantity, + ); await sendTransaction(ix); diff --git a/tokens/transfer-tokens/native/ts/instructions/transfer.ts b/tokens/transfer-tokens/native/ts/instructions/transfer.ts index 8b60694f3..6b25a177f 100644 --- a/tokens/transfer-tokens/native/ts/instructions/transfer.ts +++ b/tokens/transfer-tokens/native/ts/instructions/transfer.ts @@ -21,7 +21,7 @@ export function createTransferTokensInstruction( fromAssociatedTokenAccount: Address, toAssociatedTokenAccount: Address, owner: TransactionSigner, - recipient: TransactionSigner, + recipient: Address, payer: TransactionSigner, programId: Address, quantity: bigint, @@ -33,7 +33,7 @@ export function createTransferTokensInstruction( { address: fromAssociatedTokenAccount, role: AccountRole.WRITABLE }, { address: toAssociatedTokenAccount, role: AccountRole.WRITABLE }, { address: owner.address, role: AccountRole.WRITABLE_SIGNER, signer: owner }, - { address: recipient.address, role: AccountRole.WRITABLE_SIGNER, signer: recipient }, + { address: recipient, role: AccountRole.READONLY }, { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, { address: TOKEN_PROGRAM_ADDRESS, role: AccountRole.READONLY },