Skip to content

fix(transfer-tokens): stop requiring the recipient to sign native token transfers - #1

Draft
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/transfer-tokens-native-recipient-signer
Draft

fix(transfer-tokens): stop requiring the recipient to sign native token transfers#1
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/transfer-tokens-native-recipient-signer

Conversation

@SwineCoder101

@SwineCoder101 SwineCoder101 commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Superseded — do not merge as-is. Upstream merged the same fix in solana-foundation#695 (73219fec, "native transfer wrongly requires the recipient to co-sign") after this branch was cut, so this PR now conflicts with main. Upstream's version passes &[] as signer_pubkeys; this branch passes &[owner.key] — both are correct for a single-owner authority. What this branch still adds on top of solana-foundation#695: the regression test Transfer tokens to a wallet that does not sign the transaction! and dropping the unused mint_account/recipient from the CPI account list. Rebase onto main and keep only those parts, or close.


Bug: native transfer_tokens requires the recipient to sign the transaction

The native variant of transfer-tokens lists the recipient wallet as a required signer of the SPL Token Transfer CPI. The runtime rejects that CPI with PrivilegeEscalation unless the recipient co-signed the outer transaction, so the program can only send tokens to wallets that are present to sign. That defeats the purpose of a transfer; the Anchor and Pinocchio variants only require the sender's signature. The TypeScript client masked the bug by always attaching the recipient keypair as a writable signer.

Severity: functional. Anyone calling the program with a recipient they do not control (the normal case) hits the failure. There is no fund-loss vector, but the instruction is unusable as a transfer.

Affected

  • native/ only.
    • tokens/transfer-tokens/native/program/src/instructions/transfer.rs
    • tokens/transfer-tokens/native/ts/instructions/transfer.ts
    • tokens/transfer-tokens/native/tests/test.ts
  • anchor/ and pinocchio/ are correct and unchanged (recipient is a plain non-signer account in both).

Functionality

TransferTokens { quantity } creates the recipient's associated token account if it does not exist (paid by payer), then moves quantity tokens from the owner's ATA to the recipient's ATA. Only the owner of the source token account has to authorise the transfer.

The bug

native/program/src/instructions/transfer.rs:60-66 built the CPI as

token_instruction::transfer(
    token_program.key,
    from_associated_token_account.key,
    to_associated_token_account.key,
    owner.key,
    &[owner.key, recipient.key],
    args.quantity,
)

The fifth argument is signer_pubkeys. Including recipient.key there marks the recipient wallet as is_signer = true in the inner instruction. Because the program is not the recipient and holds no seeds for it, the runtime only allows that meta if the recipient signed the outer transaction. When it did not, invoke fails with InstructionError(0, PrivilegeEscalation) ("Cross-program invocation with unauthorized signer or writable account") and the whole transfer is rolled back, including the ATA creation.

native/ts/instructions/transfer.ts:24,36 took recipient: TransactionSigner and emitted it as AccountRole.WRITABLE_SIGNER, and the existing tests passed the recipient keypair, so the suite never exercised the real-world case.

Reproduce

cd tokens/transfer-tokens/native
pnpm install --frozen-lockfile
pnpm build-and-test

Test: Transferring Tokens > Transfer tokens to a wallet that does not sign the transaction! (native/tests/test.ts). Against the unfixed program:

  6 passing (32ms)
  1 failing

  1) Transferring Tokens
       Transfer tokens to a wallet that does not sign the transaction!:
     AssertionError: transaction failed: FailedTransactionMetadata(FailedTransactionMetadata { err: InstructionError(0, PrivilegeEscalation), ...
       "3y7tHTYtkWPLzq8Ewonim4hkGXDBfpxiD9YXjDLHiNnG's signer privilege escalated",
       "Program HPzintwjcjbgqi9LteCKY1Dkkzn6PLTQ4anfDPcTYKAj failed: Cross-program invocation with unauthorized signer or writable account"

Fix

  • transfer.rs: signer_pubkeys is now &[owner.key], matching the Anchor/Pinocchio variants. The SPL Transfer instruction only references source, destination and authority, so mint_account and recipient were also dropped from the CPI account list; recipient is still used for the ATA creation CPI, where it is a plain non-signer.
  • ts/instructions/transfer.ts: recipient is now an Address with AccountRole.READONLY. Nothing writes the recipient wallet itself (the ATA creation writes the recipient ATA, which is already WRITABLE), so it needs neither signer nor writable privileges.
  • tests/test.ts: the existing Transfer tokens to another wallet! / Transfer NFT to another wallet! tests previously had the recipient keypair sign; they now pass recipientWallet.address. The new test transfers to a freshly generated wallet that never signs and has no lamports, and asserts the recipient ATA balance equals quantity and the sender ATA decreased by quantity.

Verification

cd tokens/transfer-tokens/native
pnpm install --frozen-lockfile
pnpm build-and-test
pnpm exec tsc --noEmit
cargo clippy -p transfer-tokens-program -- -D warnings   # from the repo root
  Transferring Tokens
    ✔ Create an SPL Token!
    ✔ Create an NFT!
    ✔ Mint some tokens to your wallet!
    ✔ Mint the NFT to your wallet!
    ✔ Transfer tokens to another wallet!
    ✔ Transfer NFT to another wallet!
    ✔ Transfer tokens to a wallet that does not sign the transaction!

  7 passing (88ms)

@SwineCoder101
SwineCoder101 marked this pull request as draft August 27, 2026 12:16
@SwineCoder101
SwineCoder101 force-pushed the fix/transfer-tokens-native-recipient-signer branch from d89c628 to 9554821 Compare August 27, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant