fix(transfer-switch): key the transfer switch on the source token account owner - #13
Open
SwineCoder101 wants to merge 2 commits into
Open
fix(transfer-switch): key the transfer switch on the source token account owner#13SwineCoder101 wants to merge 2 commits into
SwineCoder101 wants to merge 2 commits into
Conversation
…der's switch A sender whose transfer switch is off can approve a delegate whose switch is on; the delegate's transfer_checked then succeeds because the hook resolves the switch from the transfer authority rather than the source token account owner.
…ount owner
The extra account meta used Seed::AccountKey { index: 3 } (the transfer
authority) and the hook derived wallet_switch from that same account, so
a delegate with its own switch on could move tokens out of a wallet whose
switch was off. Seed the switch from the source token account's owner
field (AccountData at offset 32) and constrain wallet_switch to
source_token_account.owner in the hook.
SwineCoder101
force-pushed
the
fix/transfer-hook-transfer-switch-source-owner
branch
from
August 27, 2026 12:23
f0d7ec5 to
82fd22c
Compare
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.
Bug: transfer switch is resolved from the transfer authority, so a delegate bypasses the sender's switch
The transfer hook is meant to gate every outgoing transfer on the sender's per-wallet switch, but it derived the switch PDA from the transfer authority (account index 3 of the transfer-hook execute layout) instead of the source token account's owner. Because the authority may be a delegate, any wallet whose switch is off can
approvea delegate whose own switch is on and the delegate'stransfer_checkedmoves the tokens. This is a correctness/security bug (medium): the admin-controlled switch is trivially bypassed by the very wallet it is supposed to block, sinceApprovenever invokes the hook.Affected
anchor/(only variant)anchor/programs/transfer-switch/src/instructions/initialise_extra_account_metas_list.rsanchor/programs/transfer-switch/src/instructions/transfer_hook.rsanchor/tests/litesvm.test.tsFunctionality
switch(on)lets an admin create/toggle aTransferSwitchPDA seeded by a wallet key.transfer_hook(the Token-2022Executehook) is supposed to look up the switch of the wallet that owns the source token account and reject the transfer withSwitchNotOnwhen it is off.initialize_extra_account_metas_liststores the seed rule that Token-2022 uses to resolve which switch account to pass to the hook.The bug
initialise_extra_account_metas_list.rs:43declared the extra account asSeed::AccountKey { index: 3 }. In the execute layout[source, mint, destination, authority, extra_metas, ...], index 3 is the transfer authority, which is the owner or an approved delegate.transfer_hook.rs:43boundwallet_switchwithseeds = [wallet.key().as_ref()], i.e. the same authority account, andsource_token_account(line 20) was anUncheckedAccountnever tied towallet.Scenario: sender A has switch = off. A calls Token-2022
ApproveCheckedfor delegate D (the hook is not invoked on approve). D has switch = on (or gets one; any wallet can be switched on by the admin for a different reason). D signsTransferCheckedfrom A's token account. Token-2022 resolves the extra account asPDA([D]), the hook checks D's switch, finds it on, and A's tokens move despite A's switch being off.Reproduce
Test:
Transfer switch > delegate transfers > Delegate transfer while sender switch is off, should fail!Output against the unfixed program:
Fix
initialise_extra_account_metas_list.rs: the extra account is now seeded withSeed::AccountData { account_index: 0, data_index: 32, length: 32 }, theownerfield of the source token account. Token-2022's resolver (and@solana/spl-token'screateTransferCheckedWithTransferHookInstruction) derive the switch from the account owner regardless of who signs.transfer_hook.rs:source_token_accountis now anInterfaceAccount<TokenAccount>andwallet_switchis constrained withseeds = [source_token_account.owner.as_ref()], so even a hand-built instruction cannot substitute a different wallet's switch. Thetransferring-flag check is unchanged.The test helper
expectRevertalso swallowed its own "Expected a revert" error; it now asserts outside thetry.Verification
cargo clippy --manifest-path anchor/Cargo.toml -p transfer-switch -- -D warningsis clean.