Skip to main content
Sigvex

SPL Token Delegation Security

Detects unsafe token delegation patterns including unlimited approvals and missing revocation.

SPL Token Delegation Security

Overview

Remediation Guide: How to Fix SPL Token Delegation Security

The SPL Token delegation security detector identifies unsafe token delegation patterns where programs approve delegates with unlimited amounts, fail to validate delegate accounts, or omit revocation after delegated operations. In SPL Token, the Approve instruction grants a delegate the right to transfer tokens on behalf of the account owner. Misuse of this mechanism can allow attackers to drain entire token account balances.

Why This Is an Issue

Token delegation via Approve gives a third-party account the right to transfer up to a specified amount of tokens. Unsafe delegation patterns create severe risks:

  • Unlimited approvals (u64::MAX) allow the delegate to drain the entire account balance at any time
  • Unvalidated delegates let attackers pass their own address as the delegate, granting themselves transfer rights
  • Missing revocation leaves delegation active indefinitely, meaning a compromised delegate can drain funds long after the intended operation

These patterns are analogous to ERC-20 unlimited approval vulnerabilities but with Solana-specific characteristics around account lifetime and CPI patterns.

CWE mapping: CWE-269 (Improper Privilege Management), CWE-863 (Incorrect Authorization).

How to Resolve

Native Rust

pub fn safe_delegation(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let token_account = &accounts[0];
    let delegate = &accounts[1];
    let authority = &accounts[2];

    // Validate the delegate is the expected account
    if delegate.key != &EXPECTED_DELEGATE {
        return Err(ProgramError::InvalidArgument);
    }

    // Use exact amount, never u64::MAX
    let approve_ix = spl_token::instruction::approve(
        &spl_token::id(), token_account.key, delegate.key,
        authority.key, &[], amount, // Exact amount needed
    )?;
    invoke(&approve_ix, &[token_account.clone(), delegate.clone(), authority.clone()])?;

    // Perform the delegated operation...

    // IMPORTANT: revoke delegation immediately after use
    let revoke_ix = spl_token::instruction::revoke(
        &spl_token::id(), token_account.key, authority.key, &[],
    )?;
    invoke(&revoke_ix, &[token_account.clone(), authority.clone()])?;

    Ok(())
}

Anchor

use anchor_spl::token::{self, Approve, Revoke};

pub fn safe_delegation(ctx: Context<SafeDelegation>, amount: u64) -> Result<()> {
    // Approve exact amount
    token::approve(ctx.accounts.into_approve_context(), amount)?;

    // Perform operation...

    // Revoke immediately
    token::revoke(ctx.accounts.into_revoke_context())?;
    Ok(())
}

Examples

Vulnerable Code

pub fn approve_delegate(accounts: &[AccountInfo]) -> ProgramResult {
    let token_account = &accounts[0];
    let delegate = &accounts[1]; // Not validated!
    let authority = &accounts[2];

    // VULNERABLE: unlimited approval to unvalidated delegate, no revocation
    let ix = spl_token::instruction::approve(
        &spl_token::id(), token_account.key, delegate.key,
        authority.key, &[], u64::MAX,
    )?;
    invoke(&ix, &[token_account.clone(), delegate.clone(), authority.clone()])?;
    Ok(())
}

Fixed Code

pub fn approve_delegate(accounts: &[AccountInfo], exact_amount: u64) -> ProgramResult {
    let delegate = &accounts[1];

    // Validate delegate
    if delegate.key != &EXPECTED_DELEGATE { return Err(ProgramError::InvalidArgument); }

    // Approve exact amount
    let ix = spl_token::instruction::approve(
        &spl_token::id(), accounts[0].key, delegate.key,
        accounts[2].key, &[], exact_amount,
    )?;
    invoke(&ix, &[accounts[0].clone(), delegate.clone(), accounts[2].clone()])?;

    // ... perform operation, then revoke
    let revoke_ix = spl_token::instruction::revoke(
        &spl_token::id(), accounts[0].key, accounts[2].key, &[],
    )?;
    invoke(&revoke_ix, &[accounts[0].clone(), accounts[2].clone()])?;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "spl-token-delegation-security",
  "severity": "high",
  "confidence": 0.80,
  "description": "Token delegation to unvalidated account without subsequent revocation. Delegate retains transfer rights indefinitely.",
  "location": { "function": "approve_delegate", "offset": 1 }
}

Detection Methodology

  1. Instruction classification: Identifies Approve (discriminator 4), ApproveChecked (13), and Revoke (5) CPI calls.
  2. Validation tracking: Tracks CheckOwner, CheckSigner, and CheckKey statements to determine which accounts are validated.
  3. Amount analysis: Extracts approval amounts and flags unlimited values (u64::MAX or values exceeding 99% of u64::MAX).
  4. Revocation analysis: Checks whether approval operations have corresponding Revoke operations within the same function.

Limitations

  • Simplified instruction data parsing may not extract approval amounts from complex instruction layouts.
  • Revocation in a separate transaction or function is not tracked.
  • PDA delegates (program-controlled) may be flagged even though they are inherently safe.

References