Token-2022 Immutable Owner Remediation
Overview
Related Detector: Token-2022 Immutable Owner
Legacy SPL Token allows a token account’s owner to be reassigned with SetAuthority. For custody programs, vaults, and escrows, that is a liability: an owner change on a vault token account redirects every future withdrawal. Token-2022’s ImmutableOwner extension removes the hazard by making the runtime reject any owner change — but only if the extension is initialized, and it can only be initialized before the token account itself. The remediation is twofold: initialize the extension when creating protocol-controlled accounts, and verify its presence on token accounts your program accepts from users.
Recommended Fix
Before (Vulnerable)
// Plain account creation — owner can later be reassigned
invoke(
&spl_token_2022::instruction::initialize_account3(
token_program.key, vault.key, mint.key, authority.key,
)?,
&[vault.clone(), mint.clone()],
)?;
After (Fixed)
// 1. Allocate with space for the extension
let space = ExtensionType::try_calculate_account_len::<Account>(
&[ExtensionType::ImmutableOwner],
)?;
create_account(payer, vault, rent.minimum_balance(space), space as u64, token_program.key)?;
// 2. Initialize the extension FIRST — this must precede account init
invoke(
&spl_token_2022::instruction::initialize_immutable_owner(token_program.key, vault.key)?,
&[vault.clone()],
)?;
// 3. Then initialize the token account
invoke(
&spl_token_2022::instruction::initialize_account3(
token_program.key, vault.key, mint.key, authority.key,
)?,
&[vault.clone(), mint.clone()],
)?;
Ordering is enforced by the token program: extension initialization on an already-initialized account fails, so a wrong order cannot be patched after the fact — the account must be recreated.
Alternative Mitigations
- Use Associated Token Accounts. ATAs created by the associated token program under Token-2022 include
ImmutableOwnerautomatically, and their address is derived from the owner — either property alone prevents silent owner swaps. - Verify on acceptance. When users supply existing token accounts, check the extension before treating the account as long-lived collateral:
let state = StateWithExtensions::<Account>::unpack(&account_data)?;
require!(
state.get_extension::<ImmutableOwner>().is_ok(),
ErrorCode::OwnerNotImmutable
);
Common Mistakes
Mistake: Initializing the Extension After the Account
initialize_account3(...)?; // account initialized
initialize_immutable_owner(...)?; // WRONG: fails — too late
Mistake: Allocating Legacy-Sized Accounts
Allocating exactly 165 bytes (the legacy account size) leaves no room for extension data, so extension initialization fails. Always compute the size with try_calculate_account_len and the full extension list.
Mistake: Assuming Token-2022 Implies Immutability
The extension is opt-in. A token account owned by the Token-2022 program without ImmutableOwner is exactly as reassignable as a legacy account.