fix(transfer-tokens): native transfer wrongly requires the recipient to co-sign - #695
Open
NikkiAung wants to merge 1 commit into
Open
Conversation
The native implementation's transfer_tokens instruction built the SPL
Token transfer CPI with signer_pubkeys = [owner, recipient]. Passing a
non-empty signer_pubkeys list marks the authority account as a
non-signer and instead treats it as a multisig whose members must each
co-sign — but owner is a plain wallet, not a multisig account. The
practical effect: the transfer only succeeded if the recipient also
signed the transaction, which defeats the purpose of a token transfer
(a recipient should never need to approve incoming tokens).
Confirmed empirically: with the recipient not signing, the CPI is
rejected at the runtime privilege-check stage ("Cross-program
invocation with unauthorized signer or writable account") before the
SPL Token program even runs. The anchor and pinocchio implementations
of this same example already pass signer_pubkeys = [] and never
require the recipient to sign.
Fix: pass an empty signer_pubkeys list, matching owner's actual role
as a single-signer authority. Updated the TS instruction builder and
test to pass the recipient's address instead of a signer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
Greptile SummaryThis PR corrects the native token-transfer authority model so only the sending wallet authorizes a transfer.
Confidence Score: 5/5The PR appears safe to merge with no actionable correctness or security issues identified. The Rust CPI, TypeScript account metadata, and test now consistently enforce sender authorization without requiring recipient consent, while preserving the account order and writable destination account needed for transfers and associated-token-account creation. Important Files Changed
Reviews (1): Last reviewed commit: "fix(transfer-tokens): stop requiring the..." | Re-trigger Greptile |
Contributor
Author
|
Hey @dev-jodee — this one's ready for review whenever you have a chance. CI is green and Greptile's automated pass found no blocking issues. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The
nativeimplementation oftokens/transfer-tokens'stransfer_tokensinstruction builds the SPL TokenTransferCPI like this:Passing a non-empty
signer_pubkeyslist tells the SPL Token program theauthorityaccount (owner) is a multisig account, and marksowneritself as a non-signer in the CPI's account metas — the listed pubkeys (owner,recipient) are appended as the individual co-signers of that multisig instead.ownerhere is just a plain wallet, not a multisig account, so this construction is wrong regardless of who calls it.Concrete effect, confirmed empirically: a transfer only succeeds if the recipient also signs the transaction. Without it, the CPI is rejected at the runtime privilege-check stage before the Token program even runs:
This is backwards — a recipient should never need to approve/co-sign to receive tokens sent to them. As shipped, this example teaches a pattern that makes tokens effectively impossible to transfer to any wallet that isn't present to co-sign, which defeats the purpose of the instruction. The existing test suite didn't catch this because it happened to make the recipient a signer for unrelated reasons (its account needed funding to test with).
The
anchorandpinocchioimplementations of this same example were already correct (signer_pubkeys: &[]in pinocchio, and Anchor'sTransferCPI simply never listsrecipientas an authority) — onlynativehad this bug.Fix
Pass an empty
signer_pubkeyslist, matchingowner's actual role as a plain single-signer authority — the same pattern already used by theanchorandpinocchioversions. Updated the TS instruction builder (ts/instructions/transfer.ts) to take the recipient as a plainAddressinstead of aTransactionSigner, and updated the test to pass the recipient's address without asking it to sign.Verification
signer_pubkeysfix (keeping the updated, non-signing-recipient test), rebuilt, and confirmed the transfer tests fail withCross-program invocation with unauthorized signer or writable account— then reapplied the fix and confirmed all 6 tests pass.nativelitesvm suite passes (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!).cargo fmt -p transfer-tokens-program -- --checkandcargo clippy -p transfer-tokens-program --all-targets -- -D warningsboth clean.tsc --noEmitand repo-wideprettier --checkboth clean on the touched files.🤖 Generated with Claude Code