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:
- Computes per-block validation state propagated from predecessor blocks (
SharedDataflowState). - Tracks
CheckSigner,CheckOwner, andCheckKeystatements as authorization markers. - Tracks variable aliases via
Assign { dst, src: Var(src_id) }. - For each
TransferLamports { from, amount }statement, checks whetherfromhas any authorization validation in its CFG history. - Adjusts severity:
amount = AccountLamports(full drain) is Critical; other amounts are High; PDA-derivedfromaccounts are Low. - 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.
Related Detectors
- Missing Signer Check — detects missing signer validation for privileged operations generally
- Missing Owner Check — detects missing account ownership validation
- Duplicate Mutable Accounts — detects self-transfer aliasing that can corrupt lamport accounting