fix(transfer-hook/account-data-as-seed): persist the per-owner transfer counter and mark its PDA mut - #11
Open
SwineCoder101 wants to merge 2 commits into
Open
Conversation
… PDA never advances after transfers
…er counter and mark its PDA mut
SwineCoder101
force-pushed
the
fix/transfer-hook-account-data-as-seed-persist
branch
from
August 27, 2026 12:23
3537bf0 to
35a0fc8
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 hook never persists the per-owner transfer counter
transfer_hookcomputescounter + 1into a local, logs it, and returns without writing it back, and the counter PDA is not declaredmut, so Anchor never serialises it on exit. The per-owner counter this example exists to demonstrate stays at0forever, no matter how many transfers the owner makes. Medium severity, functional bug: any client or program that reads the counter (rate limits, fees, analytics) gets a constant0.Affected
anchor/(only variant):tokens/token-2022/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rsFunctionality
initialize_extra_account_meta_listcreates aCounterAccountPDA at["counter", owner]and registers anExtraAccountMetathat derives that PDA from the source token account's owner (AccountData { account_index: 0, data_index: 32, length: 32 }). On everytransferCheckedToken-2022 CPIs intotransfer_hookwith the resolved counter PDA, which is supposed to record how many transfers that owner has made.The bug
lib.rs:69-76(pre-fix):countis a local that is only logged;counter_account.counteris never assigned. Independently,lib.rs:167-168declares the account withoutmut:so even if the field were assigned, Anchor would skip
exit()serialisation for it. TheExtraAccountMetaitself is registeredis_writable = true, so the runtime hands the account in writable and nothing fails. Scenario: an owner transfers three times, then readsCounterAccountat["counter", owner]:counter == 0. The log line claims "transferred 1 times" on every transfer.The existing test only sent one transfer and never read the counter back, so it could not catch this.
Reproduce
From
tokens/token-2022/transfer-hook/account-data-as-seed/anchor(needs port 8899 free; CI uses the same flags):Equivalent with an explicitly started validator (what was used here, because a stray
surfpoolwas already bound to 8899):The counter PDA is keyed by the wallet, so each run needs a fresh (
--reset) ledger.Test:
Counter PDA records every transfer made by the source ownerinanchor/tests/transfer-hook.tssends two moretransferChecked(three in total from the wallet) and reads the counter back. Against the unmodified program:Fix
transfer_hooknow assigns the incremented value back tocounter_account.counter(and logs that field), andTransferHook::counter_accountis marked#[account(mut, ...)]so Anchor serialises it at instruction exit. The existingcheck_is_transferringgate and the seed derivation are unchanged.readme.md'sTransferHooksnippet was updated to match. The file was also run throughcargo fmtwith the repositoryrustfmt.toml, which reflowed a few unrelated lines.The test now confirms its transfers at
confirmedbefore reading the counter atconfirmed, so the assertion reads the post-state of the last transfer rather than a stale snapshot.Verification
Same commands as above, fresh ledger, fixed program:
cargo clippy -p transfer-hook -- -D warnings(insideanchor/) andpnpm exec tsc --noEmit -p tsconfig.jsonare clean.