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
Recommended Fix
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 — Anchor 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.