Skip to content

fix(abl-token): block wallets from sending, not just receiving - #672

Open
NikkiAung wants to merge 2 commits into
solana-foundation:mainfrom
NikkiAung:fix/abl-token-sender-block
Open

fix(abl-token): block wallets from sending, not just receiving#672
NikkiAung wants to merge 2 commits into
solana-foundation:mainfrom
NikkiAung:fix/abl-token-sender-block

Conversation

@NikkiAung

@NikkiAung NikkiAung commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

allow-block-list-token's transfer hook only ever checked the destination wallet's allow/block status. A wallet explicitly blocked via init_wallet (ABWallet { allowed: false }) could still send its entire balance out through an ordinary TransferChecked to any unlisted destination — in every mint mode (Allow/Block/Threshold), since the sender's ABWallet record was never read at all.

This directly contradicts the WalletBlocked error name and the "Blocked" badge shown in the admin UI, and defeats the standard real-world reason to build a block-list token (e.g. sanctions compliance: stopping the sanctioned party from moving funds out).

Root cause: utils.rs's get_extra_account_metas() configured exactly one extra account for the hook's Execute call, seeded from account_index: 2 (the destination token account). tx_hook.rs had a source_token_account field on the accounts struct but never read it — the decision logic never had access to the sender's status in any mode.

Fix

  • utils.rs — add a second extra account seeded from the source token account's owner (account_index: 0), so Token-2022 resolves and appends both the source's and destination's ab_wallet PDAs to every Execute CPI.
  • tx_hook.rs — add source_ab_wallet to the TxHook accounts struct (ordered to match get_extra_account_metas()), and extend the decision logic so a wallet with an explicit allowed: false record is rejected regardless of which side of the transfer it's on. Allow/Threshold mode's documented "who may receive" semantics (see this example's README) are left untouched — only the block-list's own "blocked" concept, whose name and UI already imply full blocking, becomes bidirectional.
  • The decision matrix is pulled out into a standalone decide(mint_mode, source_mode, destination_mode, amount) function with unit tests, since this program has no integration-test harness that exercises tx_hook via a real hooked transfer (tests/test.rs only covers init_config/init_mint).
  • src/components/account/account-data-access.tsx — the demo frontend's useSendTokens() manually constructs the hook's extra accounts rather than relying on Token-2022's standard client-side auto-resolution. Updated it to push both the source and destination ab_wallet PDAs, in the same order the program now expects, so the demo UI stays consistent with the hardened program. This is a demo-consistency fix, not itself security-critical — any standards-conformant Token-2022 client picks up the corrected on-chain ExtraAccountMetaList automatically. Not verified via live browser/wallet testing (no validator available in this environment); it's a direct structural mirror of the pre-existing destination-account push, keyed on the sender's own public key instead.

Out of scope

block-list/pinocchio (a separate, unrelated example) has a self-documented limitation in its own README where setting up the ExtraAccountMetaList while the block list is still empty locks in "0 extra accounts" until setup-extra-metas is manually re-run after the first block. Lower severity (requires operator error, not silently wrong-by-design) and a different program — worth a follow-up PR, not bundled here.

Test plan

  • cargo checkabl-token compiles clean with the new account and function signature.
  • cargo test --lib — 9/9 pass, including the new decide() unit tests covering both sides of the block check and the existing Allow/Block/Threshold semantics.
  • Verified the key test actually catches the original bug: temporarily reintroduced the old (destination-only) logic and confirmed source_blocked_is_always_rejected fails against it, then confirmed all 9 tests pass again against the fix.
  • anchor build — IDL regenerates with exactly 7 accounts for tx_hook, in the order Token-2022 will actually append them.
  • pnpm exec tsc --noEmit on the frontend — clean, no type errors from the account-data-access.tsx change.
  • pnpm run lint / pnpm run format:check — clean (pre-existing warnings/diffs in untouched files left as-is).
  • Live end-to-end test of the frontend send flow against a real validator (not available in this environment).

get_extra_account_metas() only ever configured one extra account for
the transfer hook's Execute call, resolved from the destination token
account's owner. tx_hook() never saw the sender's ABWallet record, so
a wallet marked allowed: false could still send its full balance out
to any unlisted destination in every mint mode (Allow/Block/Threshold)
- exactly the case the WalletBlocked error and admin UI's "Blocked"
badge imply is prevented.

Add a second extra account for the source token account's owner and
extend the decision matrix so an explicitly blocked wallet is rejected
on either side, while leaving Allow/Threshold mode's documented
"who can receive" semantics untouched. The decision logic is pulled
into a standalone decide() function with unit tests covering both
sides of the block check plus the existing mode semantics, since this
program has no integration-test harness that exercises tx_hook via a
real hooked transfer.

Also fixes the frontend's manual extra-account construction in
useSendTokens() to push both the source and destination ab_wallet PDAs
in the same order the program now expects.
@NikkiAung
NikkiAung requested a review from dev-jodee as a code owner August 5, 2026 07:26
@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR extends the allow/block-list transfer hook to evaluate both the source and destination wallets and adds a migration instruction for metadata lists created with the previous one-account layout.

  • Resolves source and destination ab_wallet PDAs for hooked transfers.
  • Rejects transfers when either wallet is explicitly blocked while preserving destination-only allow-list and threshold semantics.
  • Adds an authority-gated metadata-list resize and rewrite path for existing mints.
  • Updates the frontend’s manually constructed extra accounts and adds migration and decision-logic tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported incompatibility for existing one-entry metadata lists is addressed by the new authority-gated resize and rewrite instruction.

Important Files Changed

Filename Overview
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/resize_meta_list.rs Adds an authority-checked migration that reallocates stale metadata-list accounts and rewrites the current two-wallet layout.
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/instructions/tx_hook.rs Evaluates explicit block status for both transfer participants and isolates the transfer decision logic for unit testing.
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/src/utils.rs Expands transfer-hook metadata resolution from the destination wallet alone to ordered source and destination wallet PDAs.
tokens/token-2022/transfer-hook/allow-block-list-token/anchor/programs/abl-token/tests/test.rs Adds coverage for authorized, unauthorized, idempotent, and legacy-layout metadata migration.
tokens/token-2022/transfer-hook/allow-block-list-token/src/components/account/account-data-access.tsx Updates the frontend transfer instruction to append source and destination wallet PDAs in the order expected by the hook.

Sequence Diagram

sequenceDiagram
    participant Authority
    participant ABL as ABL Program
    participant Token as Token-2022
    participant Meta as ExtraAccountMetaList

    Authority->>ABL: resize_meta_list(mint, stale meta list)
    ABL->>Token: transfer_hook_update(current program ID)
    Token-->>ABL: Confirm current hook authority
    ABL->>Meta: Fund rent and resize for two entries
    ABL->>Meta: Write source and destination wallet PDA metadata

    participant Sender
    Sender->>Token: TransferChecked
    Token->>Meta: Resolve source and destination wallet PDAs
    Token->>ABL: Execute transfer hook with both PDAs
    ABL-->>Token: Allow or reject transfer
Loading

Reviews (2): Last reviewed commit: "fix(abl-token): add resize_meta_list to ..." | Re-trigger Greptile


pub fn get_meta_list_size() -> Result<usize> {
Ok(ExtraAccountMetaList::size_of(1).map_err(|_| ProgramError::InvalidArgument)?)
Ok(ExtraAccountMetaList::size_of(2).map_err(|_| ProgramError::InvalidArgument)?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Existing metadata lists become incompatible

If any mints retain metadata-list accounts initialized before this upgrade, those accounts still resolve only the destination wallet while the upgraded hook requires both wallet accounts, causing all transfers for those mints to fail. Both setup paths use init, and there is no instruction to resize and rewrite an existing list.

Knowledge Base Used: Tokens Directory Overview

… new layout

Greptile review on PR solana-foundation#672 flagged that extra_metas_account is a
fixed-size PDA created once by init_mint/attach_to_mint: mints created
before the sender-side check was added are left with the old,
undersized (one-entry) account, so their transfers start failing the
hook's account-count check after the program upgrades - the fix has
no effect for them without a migration path.

Add resize_meta_list, which reallocates extra_metas_account to the
current get_meta_list_size() and rewrites its contents via
ExtraAccountMetaList::update (not ::init, which only handles the
account's first-ever write). Authorization reuses attach_to_mint's
existing pattern: a no-op transfer_hook_update CPI back to Token-2022,
which only succeeds if the caller is the mint's actual transfer-hook
authority - so this instruction needs no authority table of its own.

Tested against a litesvm account manually seeded with the old
one-entry TLV layout to confirm the migration produces byte-identical
output to a fresh init_mint, plus the idempotent (already-current-size)
case and rejection of a non-authority caller.
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