Skip to main content
Sigvex

Lamport Drain Remediation

How to prevent unauthorized SOL transfers by validating signer authorization and stored authority before any lamport modification.

Lamport Drain Remediation

Overview

Lamport drain vulnerabilities arise when a Solana program transfers SOL (lamports) without verifying that the source account’s authority has signed the transaction. The remediation requires checking is_signer on the authority account and verifying that the signer’s key matches the stored authority in the vault account data before any lamport modification.

Related Detector: Lamport Drain

Before (Vulnerable)

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

pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let vault = &accounts[0];
    let recipient = &accounts[1];

    // VULNERABLE: no authorization check — anyone can drain any account
    **vault.lamports.borrow_mut() -= amount;
    **recipient.lamports.borrow_mut() += amount;
    Ok(())
}

After (Fixed)

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    program_error::ProgramError,
    pubkey::Pubkey,
};

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

    // Check 1: authority must have signed the transaction
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Check 2: authority must match the stored authority in vault data
    let vault_data = vault.data.borrow();
    let stored_authority = Pubkey::try_from(&vault_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if authority.key != &stored_authority {
        return Err(ProgramError::InvalidAccountData);
    }

    // Safe to transfer
    **vault.lamports.borrow_mut() -= amount;
    **recipient.lamports.borrow_mut() += amount;
    Ok(())
}

Alternative Mitigations

Anchor has_one and close constraints — an account-validation framework handles authorization and lamport transfer declaratively:

use anchor_lang::prelude::*;

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

#[derive(Accounts)]
pub struct CloseVault<'info> {
    pub authority: Signer<'info>,            // Enforces is_signer
    #[account(
        mut,
        has_one = authority,                  // Enforces vault.authority == authority.key()
        close = recipient                     // Transfers all lamports and closes the account
    )]
    pub vault: Account<'info, Vault>,
    #[account(mut)]
    pub recipient: SystemAccount<'info>,
}

pub fn close_vault(ctx: Context<CloseVault>) -> Result<()> {
    // Anchor validates authority via Signer<'info> + has_one constraint
    // Lamport transfer is handled by close = recipient
    Ok(())
}

PDA-signed transfers — for program-owned accounts, use invoke_signed with the PDA’s seeds instead of checking is_signer:

use solana_program::program::invoke_signed;
use solana_program::system_instruction;

pub fn transfer_from_pda(
    accounts: &[AccountInfo],
    amount: u64,
    pda_bump: u8,
) -> ProgramResult {
    let pda_account = &accounts[0];
    let recipient = &accounts[1];

    // PDA signs via invoke_signed — no is_signer check needed
    invoke_signed(
        &system_instruction::transfer(pda_account.key, recipient.key, amount),
        &[pda_account.clone(), recipient.clone()],
        &[&[b"vault", &[pda_bump]]],
    )
}

Common Mistakes

Checking is_signer but not verifying key — if any signer can authorize a transfer, an attacker can create their own account, sign with it, and drain any vault whose authority is not verified.

Checking key but not is_signer — an attacker can pass an account whose key matches the stored authority but that has not signed the transaction.

Forgetting to zero lamports on close — when closing an account, set both lamports = 0 and the data to zeros; otherwise the runtime may not fully close the account.

References