Token-2022 CPI Guard Bypass Remediation
Overview
Related Detector: Token-2022 CPI Guard Bypass
The CPI Guard extension lets a token account owner block program-initiated operations performed through cross-program invocation. When it is enabled, a transfer, approval, or authority change issued by another program on the owner’s behalf is rejected unless the owner signs the transaction directly. A program that moves user tokens through CPI without anticipating an active guard will have its instruction rejected at runtime. The fix is to inspect the source account for an active CPI Guard before the CPI and handle the guarded case with an explicit, program-defined error.
Recommended Fix
Before (Vulnerable)
// Transfers through CPI without considering CPI Guard. If the source
// account has CPI Guard enabled, this CPI is rejected and the failure
// is not handled, so callers see an opaque transaction error.
fn sweep(ctx: Context<Sweep>) -> Result<()> {
transfer_checked_via_cpi(&ctx)?;
Ok(())
}
After (Fixed)
use spl_token_2022::extension::{cpi_guard::CpiGuard, BaseStateWithExtensions, StateWithExtensions};
use spl_token_2022::state::Account as Token2022Account;
fn sweep(ctx: Context<Sweep>) -> Result<()> {
let data = ctx.accounts.source.try_borrow_data()?;
let state = StateWithExtensions::<Token2022Account>::unpack(&data)?;
if state.get_extension::<CpiGuard>().is_ok() {
// The owner must sign directly; a program-initiated CPI cannot
// move these tokens. Surface this as an actionable error.
return err!(VaultError::SourceRequiresDirectSignature);
}
drop(data);
transfer_checked_via_cpi(&ctx)?;
Ok(())
}
This works because the program decides what happens on a guarded account instead of delegating that decision to the runtime. The guard check runs before any bookkeeping, so the program never records a movement that the CPI would later reject. Callers receive a meaningful error they can act on rather than a generic failure.
Alternative Mitigations
- Offer a direct-signature path. When supporting guarded accounts is a product requirement, route the transfer instruction so the account owner signs it directly rather than having the program move tokens through CPI. This is the only flow CPI Guard permits for these accounts.
- Order effects after the CPI returns. Where a guarded account cannot be ruled out, perform all internal accounting only after the transfer CPI returns successfully, so a rejected transfer leaves no inconsistent state behind.
Common Mistakes
- Swallowing the guard error and continuing. Catching the CPI failure and proceeding as if the transfer happened produces the exact phantom-movement bug the guard exists to prevent.
- Recording the transfer before the CPI returns. Updating balances or emitting a settlement event ahead of a successful CPI lets a guard rejection desynchronise on-chain state from program bookkeeping.
- Checking the wrong account. The guard lives on the source token account, not the mint or the destination; inspect the account whose tokens are being moved.