SPL Token Close Authority Bypass Remediation
How to fix missing close_authority validation when closing token accounts.
SPL Token Close Authority Bypass Remediation
Overview
Related Detector: SPL Token Close Authority Bypass
Missing close_authority validation allows unauthorized account closure and rent theft. The fix requires reading the token account’s close_authority field and verifying the signer matches before executing the close operation.
Recommended Fix
Before (Vulnerable)
// No close_authority check -- any signer could close
invoke(&spl_token::instruction::close_account(
&spl_token::id(), token_account.key, destination.key, authority.key, &[],
)?, accounts)?;
After (Fixed)
let token_data = Account::unpack(&token_account.data.borrow())?;
// Check close_authority field
match token_data.close_authority {
COption::Some(close_auth) => {
require!(*authority.key == close_auth, InvalidCloseAuthority);
}
COption::None => {
// No close_authority set -- owner can close
require!(*authority.key == token_data.owner, InvalidOwner);
}
}
require!(authority.is_signer, MissingSignature);
invoke(&spl_token::instruction::close_account(
&spl_token::id(), token_account.key, destination.key, authority.key, &[],
)?, accounts)?;
Alternative Mitigations
1. Anchor has_one constraint
#[derive(Accounts)]
pub struct CloseToken<'info> {
#[account(
mut,
close = destination,
has_one = close_authority @ ErrorCode::InvalidCloseAuthority
)]
pub token_account: Account<'info, TokenAccount>,
pub close_authority: Signer<'info>,
/// CHECK: rent destination
pub destination: AccountInfo<'info>,
}
2. Set close_authority to PDA
Set the close_authority to a program-controlled PDA so only your program can close accounts:
spl_token::instruction::set_authority(
&spl_token::id(), token_account.key,
Some(&pda_authority), AuthorityType::CloseAccount,
current_authority.key, &[],
)?;
Common Mistakes
Mistake 1: Checking owner instead of close_authority
// WRONG: if close_authority is set, owner may NOT be able to close
if *authority.key != token_data.owner { return Err(...); }
// CORRECT: check close_authority first, fall back to owner
Mistake 2: Not handling COption::None
// WRONG: unwrapping close_authority without checking if set
let close_auth = token_data.close_authority.unwrap(); // Panics if None