Skip to content

fix(abl-token): require ImmutableOwner in the hook, exempt the permanent delegate from the source block, honour mint_authority - #2

Open
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/abl-token-immutable-owner-and-permanent-delegate
Open

fix(abl-token): require ImmutableOwner in the hook, exempt the permanent delegate from the source block, honour mint_authority#2
SwineCoder101 wants to merge 2 commits into
mainfrom
fix/abl-token-immutable-owner-and-permanent-delegate

Conversation

@SwineCoder101

Copy link
Copy Markdown
Owner

Bug: transfer hook is bypassable via SetAuthority, blocks permanent-delegate clawbacks, and init_mint ignores mint_authority

The abl-token transfer hook keys its allow/block list on the token-account owner but never requires the ImmutableOwner extension, so anyone holding a plain (non-ATA) Token-2022 account can move it to a fresh wallet with SetAuthority(AccountOwner) - an instruction the hook never sees - and route tokens out of, or into, a blocked wallet. Separately, since solana-foundation#672 the hook rejects every transfer whose source wallet is blocked, including ones signed by the mint's permanent delegate, so the issuer cannot claw back from a blocked wallet without first unblocking it. Finally InitMintArgs.mint_authority is accepted but ignored: the payer always ends up as mint authority. These are correctness bugs (medium / medium / low); the first is exploitable by any blocked holder or anyone wanting to fund a blocked wallet.

Affected

  • anchor/ variant, program programs/abl-token:
    • anchor/programs/abl-token/src/instructions/tx_hook.rs
    • anchor/programs/abl-token/src/instructions/init_mint.rs
    • anchor/programs/abl-token/src/errors.rs
  • Regenerated client artifacts for the new error code: idl/abl_token.json, src/generated/errors/ablToken.ts.

Functionality

tx_hook is the Token-2022 transfer-hook Execute handler. It resolves the ab_wallet PDA of the source and destination token-account owners (utils.rs) and decides whether the transfer may proceed given the mint's mode (Allow / Block / Mixed threshold). A wallet with allowed: false must be unable to send or receive. The mint is created with a permanent delegate (init_mint.rs) so the issuer can claw tokens back, and InitMintArgs lets the caller choose the mint authority.

The bug

  1. No ImmutableOwner requirement - utils.rs:19-34 seeds both ab_wallet PDAs from the token account's owner field (data_index: 32), and tx_hook.rs:40-41 decodes those PDAs. Nothing checks that the owner is immutable. Token-2022 lets a token account without the ImmutableOwner extension change its owner via SetAuthority(AccountOwner), which is not a transfer and never invokes the hook. So:
    • a blocked wallet holding a plain token account reassigns it to a fresh wallet; the fresh wallet's ab_wallet PDA is empty, and the hook lets the transfer through;
    • a sender transfers to an unlisted "mule" account, whose owner then reassigns it to the blocked wallet.
      The pinocchio sibling (block-list/pinocchio/program/src/instructions/tx_hook.rs) explicitly refuses both sides without the extension.
  2. Permanent delegate cannot claw back - tx_hook.rs:115 (pre-fix decide) returns WalletBlocked whenever the source wallet is blocked, ignoring who authorised the transfer. owner_delegate (the transfer authority, tx_hook.rs:23) is never consulted and the mint's PermanentDelegate extension is never read, so the issuer's clawback path advertised in the README is dead for exactly the wallets it is meant for. The pinocchio sibling exempts the permanent delegate.
  3. mint_authority ignored - init_mint.rs:26 sets mint::authority = payer.key() and args.mint_authority (init_mint.rs:111) is never used, so the value the UI lets the user choose has no effect.

Reproduce

cd tokens/token-2022/transfer-hook/allow-block-list-token && pnpm install --frozen-lockfile
cd anchor && anchor build --ignore-keys && cargo test -p abl-token --test test

Tests: hook_rejects_a_source_account_without_immutable_owner, hook_rejects_a_destination_account_without_immutable_owner, permanent_delegate_can_claw_back_from_a_blocked_wallet, init_mint_honours_the_mint_authority_argument (all in anchor/programs/abl-token/tests/test.rs).

Against the unmodified program:

test init_mint_honours_the_mint_authority_argument ... FAILED
test hook_rejects_a_source_account_without_immutable_owner ... FAILED
test permanent_delegate_can_claw_back_from_a_blocked_wallet ... FAILED
test hook_rejects_a_destination_account_without_immutable_owner ... FAILED

---- init_mint_honours_the_mint_authority_argument stdout ----
assertion `left == right` failed: the mint authority must be the one passed in InitMintArgs, not the payer
  left: Some(Dh7XUrWbhA1EDkkhNBpCjDNSBAbR3GJTEPEeCxWpwV8h)
 right: Some(11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs)

---- hook_rejects_a_source_account_without_immutable_owner stdout ----
a source token account without ImmutableOwner must be rejected: TransactionMetadata { ... logs: [
  "Program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb invoke [1]", "Program log: Instruction: TransferChecked",
  "Program 3ku1ZEGvBEEfhaYsAzBZuecTPEa58ZRhoVqHVGpGxVGi invoke [2]", "Program log: Instruction: TxHook",
  "Program 3ku1ZEGvBEEfhaYsAzBZuecTPEa58ZRhoVqHVGpGxVGi success", ... ] }

---- permanent_delegate_can_claw_back_from_a_blocked_wallet stdout ----
the permanent delegate must be able to claw back from a blocked wallet: FailedTransactionMetadata { err: InstructionError(0, Custom(6003)), ... logs: [
  ... "Program log: AnchorError occurred. Error Code: WalletBlocked. Error Number: 6003. Error Message: Wallet blocked.", ... ] }

test result: FAILED. 8 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out

The passing 8 include the regression guards blocked_wallet_cannot_send (solana-foundation#672 semantics), unlisted_wallets_can_transfer_in_block_mode and permanent_delegate_cannot_send_to_a_blocked_wallet.

Fix

  • tx_hook.rs: before consulting the lists, unpack both token accounts with StateWithExtensions::<Account> and require get_extension::<ImmutableOwner>() on each, failing with the new ABListError::ImmutableOwnerRequired otherwise. Token-2022 ATAs always carry the extension (the UI only ever creates ATAs), so ordinary users are unaffected; only accounts whose owner could be swapped are refused.
  • tx_hook.rs: read the mint's PermanentDelegate extension and pass authority_is_permanent_delegate (transfer authority == delegate) into decide. A blocked destination is still rejected unconditionally; a blocked source is rejected unless the permanent delegate authorised the transfer. The mode rules (Allow / Threshold) still apply to the destination for the delegate as well. fix(abl-token): block wallets from sending, not just receiving solana-foundation/program-examples#672's behaviour for a plain blocked sender is unchanged.
  • init_mint.rs: the payer must be mint authority while the metadata is initialised, so after the metadata and meta-list setup the mint authority is handed to args.mint_authority with a SetAuthority(MintTokens) CPI when it differs from the payer.
  • errors.rs: ImmutableOwnerRequired appended (existing error numbers unchanged); idl/abl_token.json and src/generated/errors/ablToken.ts regenerated via pnpm run generate-client.
  • decide unit tests updated for the new parameter, plus two new ones covering the delegate exemption and that it does not extend to the destination.

Verification

cd tokens/token-2022/transfer-hook/allow-block-list-token/anchor
anchor build --ignore-keys
cargo test -p abl-token && ../node_modules/.bin/mocha --import=tsx -t 1000000 tests/**/*.ts
cargo fmt -p abl-token -- --check && cargo clippy -p abl-token -- -D warnings
running 11 tests
test instructions::tx_hook::tests::permanent_delegate_is_still_subject_to_destination_rules ... ok
test instructions::tx_hook::tests::permanent_delegate_may_claw_back_from_a_blocked_source ... ok
test instructions::tx_hook::tests::source_blocked_is_always_rejected ... ok
test instructions::tx_hook::tests::destination_blocked_is_always_rejected ... ok
...
test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

running 12 tests
test init_mint_honours_the_mint_authority_argument ... ok
test unlisted_wallets_can_transfer_in_block_mode ... ok
test permanent_delegate_cannot_send_to_a_blocked_wallet ... ok
test hook_rejects_a_source_account_without_immutable_owner ... ok
test blocked_wallet_cannot_send ... ok
test hook_rejects_a_destination_account_without_immutable_owner ... ok
test permanent_delegate_can_claw_back_from_a_blocked_wallet ... ok
...
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

  abl-token (Kit client, via LiteSVM)
    ✔ initializes the config, owned by the payer
    ✔ adds a wallet to the list, then removes it by wallet address alone
    ✔ rejects removing a wallet for a caller who is not the config authority

  3 passing

…cks permanent-delegate clawbacks, and init_mint ignores mint_authority

Adds litesvm integration tests that drive real Token-2022 hooked
transfers through tx_hook:

- hook_rejects_a_source_account_without_immutable_owner /
  hook_rejects_a_destination_account_without_immutable_owner: a plain
  (non-ATA) token account without the ImmutableOwner extension can be
  reassigned with SetAuthority(AccountOwner), which never invokes the
  hook, so a blocked wallet's tokens leave via a fresh owner and tokens
  can be routed into a blocked wallet via an unlisted mule account.
- permanent_delegate_can_claw_back_from_a_blocked_wallet: the mint's
  permanent delegate is rejected with WalletBlocked when clawing back
  from a blocked wallet.
- init_mint_honours_the_mint_authority_argument: the mint authority is
  always the payer; InitMintArgs.mint_authority is ignored.

Plus guards that already pass: blocked_wallet_cannot_send,
unlisted_wallets_can_transfer_in_block_mode and
permanent_delegate_cannot_send_to_a_blocked_wallet.
…ent delegate from the source block, honour mint_authority

- tx_hook: both token accounts must carry the ImmutableOwner extension
  (new ABListError::ImmutableOwnerRequired). The lists are keyed on the
  token-account owner, and a mutable owner can be swapped with
  SetAuthority(AccountOwner) without the hook ever running, so a blocked
  wallet could send via a reassigned account and receive via a mule.
  Token-2022 ATAs always have the extension, so ordinary users are
  unaffected.
- tx_hook: read the mint's PermanentDelegate extension and let the
  delegate move tokens out of a blocked wallet (clawback). A blocked
  destination and the Allow/Threshold rules still apply to it; a plain
  blocked sender is still rejected as since solana-foundation#672.
- init_mint: hand the mint authority to InitMintArgs.mint_authority with
  a SetAuthority(MintTokens) CPI after the metadata is initialised,
  instead of silently leaving it with the payer.
- Regenerate idl/abl_token.json and src/generated/errors for the new
  error code; update decide() unit tests.
@SwineCoder101
SwineCoder101 force-pushed the fix/abl-token-immutable-owner-and-permanent-delegate branch from 0f5cd93 to b46a16e 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