Skip to main content
Sigvex

Lamport Drain

Detects unauthorized lamport transfers where an account's SOL balance is transferred without proper signer, key, or owner authorization checks.

Lamport Drain

Overview

Remediation Guide: How to Fix Lamport Drain

The lamport drain detector identifies Solana program instructions that transfer SOL (lamports) between accounts without verifying that the sending account’s authority has signed the transaction or that the program is the legitimate owner of the account. In Solana, lamports are the native currency. Programs can transfer lamports by directly modifying account.lamports. Without authorization checks, any caller can craft a transaction that passes an arbitrary account as the source and drain its entire SOL balance.

The detector uses CFG-based dataflow analysis: it propagates validation state (CheckSigner, CheckOwner, CheckKey) through basic blocks and emits a finding for any TransferLamports statement where no authorization has been established for the sending account. Severity is Critical for full-balance drains (amount = AccountLamports) and High for partial drains.

Why This Is an Issue

Lamports represent real monetary value — draining an account’s SOL balance is equivalent to theft. Because Solana programs receive accounts as an opaque array and must validate them manually, any program that transfers lamports without checking the source account’s authorization is exploitable by any user who can call the instruction.

The vulnerability is particularly common in account-closing logic: programs that close an account and return its rent to a recipient must ensure the authority to close has been verified. Without this check, an attacker can pass any account as the one to close and receive its lamports.

PDA-derived accounts have a lower risk level because the Solana runtime enforces that only the owning program can debit a PDA — the detector assigns Low severity and 0.25 confidence for PDA-sourced transfers.

How to Resolve

Verify that the account owner has signed the transaction before any lamport transfer. Additionally, verify that the signing account matches a stored authority field in the account data.

// Before: Vulnerable — no authorization check
pub fn emergency_withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let source = &accounts[0];
    let destination = &accounts[1];
    // No signer check, no key check — anyone can drain any account
    **source.lamports.borrow_mut() -= amount;
    **destination.lamports.borrow_mut() += amount;
    Ok(())
}

// After: Fixed — signer and key validation before transfer
pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let owner = &accounts[0];
    let vault = &accounts[1];
    let destination = &accounts[2];

    if !owner.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let vault_data = vault.data.borrow();
    let stored_authority = Pubkey::from_slice(&vault_data[0..32])?;
    if owner.key != &stored_authority {
        return Err(ProgramError::InvalidAccountData);
    }

    **vault.lamports.borrow_mut() -= amount;
    **destination.lamports.borrow_mut() += amount;
    Ok(())
}

Examples

Vulnerable Code

use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};

// VULNERABLE: full-balance drain without any authorization
pub fn close_account(accounts: &[AccountInfo]) -> ProgramResult {
    let account_to_close = &accounts[0];
    let recipient = &accounts[1];

    // CRITICAL: draining entire balance — no signer, owner, or key check
    let balance = **account_to_close.lamports.borrow();
    **account_to_close.lamports.borrow_mut() = 0;
    **recipient.lamports.borrow_mut() += balance;
    Ok(())
}

Fixed Code

use anchor_lang::prelude::*;

#[account]
pub struct Vault {
    pub authority: Pubkey,
    pub balance: u64,
}

#[derive(Accounts)]
pub struct CloseVault<'info> {
    // Anchor's Signer<'info> enforces is_signer = true at deserialization
    pub authority: Signer<'info>,
    #[account(
        mut,
        // has_one ensures authority.key() == vault.authority
        has_one = authority,
        // close = recipient transfers remaining lamports and zeroes the account
        close = recipient
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub recipient: SystemAccount<'info>,
}

pub fn close_vault(ctx: Context<CloseVault>) -> Result<()> {
    // Anchor handles the lamport transfer via close = recipient
    // authority is verified via Signer + has_one constraint
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "lamport-drain",
  "severity": "critical",
  "confidence": 0.75,
  "description": "TransferLamports statement drains full account balance (amount = AccountLamports) from account variable var_0 at statement index 3 in function close_account. No CheckSigner, CheckOwner, or CheckKey statement found for var_0 in any predecessor block.",
  "location": {
    "function": "close_account",
    "offset": 0
  }
}

Detection Methodology

The detector uses CFG-based dataflow analysis identical in structure to missing-signer-check:

  1. Computes per-block validation state propagated from predecessor blocks (SharedDataflowState).
  2. Tracks CheckSigner, CheckOwner, and CheckKey statements as authorization markers.
  3. Tracks variable aliases via Assign { dst, src: Var(src_id) }.
  4. For each TransferLamports { from, amount } statement, checks whether from has any authorization validation in its CFG history.
  5. Adjusts severity: amount = AccountLamports (full drain) is Critical; other amounts are High; PDA-derived from accounts are Low.
  6. Adjusts confidence: non-PDA accounts receive 0.75; PDA accounts receive 0.25.

Limitations

False positives:

  • Lamport transfers from PDAs owned by the current program are flagged with Low severity but 0.25 confidence — the runtime already enforces the authorization, so these findings rarely represent real vulnerabilities.
  • Transfers where the authorization check occurs in a separate account-loading helper function not visible in the current function’s CFG may produce false positives.

False negatives:

  • Authorization checks that use non-standard patterns (e.g., custom authority tables rather than is_signer) may not be recognized as valid checks by the detector.

References