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
Conversation
…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
force-pushed
the
fix/abl-token-immutable-owner-and-permanent-delegate
branch
from
August 27, 2026 12:23
0f5cd93 to
b46a16e
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 is bypassable via
SetAuthority, blocks permanent-delegate clawbacks, andinit_mintignoresmint_authorityThe
abl-tokentransfer hook keys its allow/block list on the token-account owner but never requires theImmutableOwnerextension, so anyone holding a plain (non-ATA) Token-2022 account can move it to a fresh wallet withSetAuthority(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. FinallyInitMintArgs.mint_authorityis 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, programprograms/abl-token:anchor/programs/abl-token/src/instructions/tx_hook.rsanchor/programs/abl-token/src/instructions/init_mint.rsanchor/programs/abl-token/src/errors.rsidl/abl_token.json,src/generated/errors/ablToken.ts.Functionality
tx_hookis the Token-2022 transfer-hookExecutehandler. It resolves theab_walletPDA of the source and destination token-account owners (utils.rs) and decides whether the transfer may proceed given the mint's mode (Allow/Block/Mixedthreshold). A wallet withallowed: falsemust be unable to send or receive. The mint is created with a permanent delegate (init_mint.rs) so the issuer can claw tokens back, andInitMintArgslets the caller choose the mint authority.The bug
ImmutableOwnerrequirement -utils.rs:19-34seeds bothab_walletPDAs from the token account's owner field (data_index: 32), andtx_hook.rs:40-41decodes those PDAs. Nothing checks that the owner is immutable. Token-2022 lets a token account without theImmutableOwnerextension change its owner viaSetAuthority(AccountOwner), which is not a transfer and never invokes the hook. So:ab_walletPDA is empty, and the hook lets the transfer through;The pinocchio sibling (
block-list/pinocchio/program/src/instructions/tx_hook.rs) explicitly refuses both sides without the extension.tx_hook.rs:115(pre-fixdecide) returnsWalletBlockedwhenever 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'sPermanentDelegateextension 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.mint_authorityignored -init_mint.rs:26setsmint::authority = payer.key()andargs.mint_authority(init_mint.rs:111) is never used, so the value the UI lets the user choose has no effect.Reproduce
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 inanchor/programs/abl-token/tests/test.rs).Against the unmodified program:
The passing 8 include the regression guards
blocked_wallet_cannot_send(solana-foundation#672 semantics),unlisted_wallets_can_transfer_in_block_modeandpermanent_delegate_cannot_send_to_a_blocked_wallet.Fix
tx_hook.rs: before consulting the lists, unpack both token accounts withStateWithExtensions::<Account>and requireget_extension::<ImmutableOwner>()on each, failing with the newABListError::ImmutableOwnerRequiredotherwise. 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'sPermanentDelegateextension and passauthority_is_permanent_delegate(transfer authority == delegate) intodecide. 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 toargs.mint_authoritywith aSetAuthority(MintTokens)CPI when it differs from the payer.errors.rs:ImmutableOwnerRequiredappended (existing error numbers unchanged);idl/abl_token.jsonandsrc/generated/errors/ablToken.tsregenerated viapnpm run generate-client.decideunit tests updated for the new parameter, plus two new ones covering the delegate exemption and that it does not extend to the destination.Verification