Skip to main content
Sigvex

Token-2022 CPI Guard Bypass

Detects Token-2022 CPI transfers that ignore the CPI Guard extension and fail to handle guarded accounts.

Token-2022 CPI Guard Bypass

Overview

The CPI Guard extension in Token-2022 (Token Extensions) lets a token account owner block certain operations from being performed through cross-program invocation (CPI). When CPI Guard is enabled, transfers, approvals, and authority changes initiated by another program on behalf of the account are rejected unless the owner signs the transaction directly. This protects users from drain attacks where a malicious or buggy program moves tokens through CPI without explicit, transaction-level consent.

This detector flags programs that issue token transfers through CPI without accounting for the possibility that the source account has CPI Guard enabled. It also flags raw invocations into the Token-2022 program that proceed without any guard awareness or error handling. In both cases the program assumes the transfer will succeed, which is not guaranteed for guarded accounts.

Why This Is an Issue

A program that transfers tokens through CPI on a CPI-Guard-protected account will have that instruction rejected at runtime. If the program does not anticipate this, the failure surfaces as an opaque transaction error rather than a meaningful state. At best this is a correctness and availability problem: deposits, settlements, or sweeps silently fail and users get stuck. At worse, a program that branches on the assumption that a CPI transfer always succeeds can be driven into an inconsistent state, where bookkeeping records a movement of funds that never actually occurred on-chain.

The risk is highest in flows that move user tokens automatically (vaults, escrows, routers, fee collectors). These are exactly the flows CPI Guard exists to protect, and they are also the flows most likely to break or behave incorrectly when the guard fires unexpectedly.

How to Resolve

Check the source account for an active CPI Guard before attempting a CPI transfer, and handle the guarded case explicitly instead of letting the CPI fail blind.

use spl_token_2022::extension::{cpi_guard::CpiGuard, BaseStateWithExtensions, StateWithExtensions};
use spl_token_2022::state::Account as Token2022Account;

// Before: transfers through CPI without considering CPI Guard.
// If the source account has CPI Guard enabled, this CPI is rejected
// at runtime and the failure is not handled.
fn sweep(ctx: Context<Sweep>) -> Result<()> {
    transfer_checked_via_cpi(&ctx)?;
    Ok(())
}

// After: inspect the source account's extensions and reject guarded
// accounts with a clear, program-defined error before attempting CPI.
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(())
}

When supporting guarded accounts is a requirement, offer a flow where the owner signs the transfer instruction directly rather than delegating it to your program through CPI. Do not silently swallow the guard error and continue, and do not record a transfer as completed before the CPI returns successfully.

Sample Sigvex Output

{
  "detector_id": "token-2022-cpi-guard",
  "severity": "high",
  "confidence": 0.75,
  "description": "Token transfer via CPI without checking if the source account has CPI Guard enabled.",
  "location": {
    "function": "sweep",
    "offset": 0
  }
}

Detection Methodology

The detector operates on the reconstructed intermediate representation of each program function recovered from the program binary. It runs two passes:

  1. Guard-awareness pass. It scans the function for evidence that the program inspects CPI Guard state or handles the corresponding error: extension-data reads at the offset range where the CPI Guard extension lives, calls that resolve a CPI Guard extension, and branch conditions that test guard-related values. Accounts touched by such checks are recorded as guard-aware, and the function is marked as having guard error handling.

  2. Transfer pass. It then walks the cross-program invocations in the function. A CPI whose instruction discriminator corresponds to a token transfer (Transfer or TransferChecked) is flagged when its source account was never guard-checked and the function contains no guard error handling. A separate, lower-confidence finding is raised for raw invocations into the Token-2022 program that proceed without guard awareness.

Findings carry calibrated confidence. The score is reduced for functions that look like administrative or initialization entry points (which are typically gated by caller-level access control) and for read-only contexts, where a guard bypass cannot be exploited without a state-mutating CPI.

Limitations

False positives occur when guard handling is delegated to a helper the analysis cannot follow, or when the source account is known out-of-band to never enable CPI Guard. False negatives occur when the transfer discriminator is computed dynamically and cannot be resolved statically, or when the source account is passed indirectly so that the link between the guard check and the transfer is lost. Confidence is intentionally lowered for administrative and read-only functions, which can suppress legitimate findings in those contexts.

References