Lamport Drain Exploit Generator
Sigvex exploit generator that validates lamport drain vulnerabilities in Solana programs by simulating an attack that drains all lamports from an account the program fails to adequately protect, with impact reported as potential SOL loss.
Lamport Drain Exploit Generator
Overview
The lamport drain exploit generator validates findings from the lamport-drain detector by simulating an attack that drains all lamports from an account the program fails to adequately protect. The vulnerability is confirmed with confidence 0.90. The impact is reported as the total lamport balance in the exploit context, converted to SOL.
In Solana, every account stores a lamport balance that covers rent exemption and holds any SOL value. If a program allows an instruction to reduce an account’s lamports below zero or to transfer lamports to an attacker-controlled account without proper authorization, all SOL held by that account can be drained. Unlike ERC-20 token theft, lamport draining targets the native SOL stored directly in program accounts.
Severity score: 90/100 (Critical).
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Direct lamport transfer without authorization:
- A program has a
claim_rewardsinstruction that transfers lamports to a claimant. - The instruction does not verify that the claimant is the authorized beneficiary.
- An attacker calls
claim_rewardswith their own account as the claimant. - The program transfers all reward lamports to the attacker.
Rent exemption circumvention:
- A program allows users to close their data accounts and reclaim the rent lamports.
- The close instruction does not verify the recipient is the account owner.
- An attacker closes someone else’s account with their own address as the lamport recipient.
- The attacker receives the victim’s rent lamports.
Unchecked lamport subtraction:
- A program computes
account.lamports -= feewherefeeis user-supplied. - No check ensures the account has sufficient lamports to cover the fee.
- Solana’s checked arithmetic is not used.
- An attacker supplies a
feelarger than the balance. - Depending on the runtime version, lamports may underflow, corrupting the account’s lamport count.
Exploit Mechanics
The engine maps lamport-drain detector findings to the lamport drain exploit pattern (severity score 90/100).
Strategy: Construct a transaction that invokes the vulnerable instruction with an attacker-controlled recipient address. Because the program does not verify the recipient is the legitimate account owner, lamports are transferred to the attacker.
Simulation steps:
- The engine identifies the reward-claim or close instruction from the finding location.
- The exploit transaction is constructed with:
victim_rewards_account(writable),attacker_pubkeyas the destination (not the account owner). - The transaction is submitted to the program simulator.
- If the program transfers lamports to the attacker address without an ownership check, the vulnerability is confirmed with confidence 0.90.
- Impact is computed from the total lamport balance in the context accounts, converted to SOL:
impact = "Potential loss of {N} SOL". - Evidence recorded:
attacker_address,lamports_drained,victim_account_pubkey.
Lamport drain exploit transaction structure:
// No private key of the victim required — just their public key
let accounts = vec![
AccountMeta::new(victim_rewards_account, false), // victim's reward pool (writable)
AccountMeta::new(attacker_pubkey, false), // attacker as recipient (NOT owner)
AccountMeta::new_readonly(system_program_id, false),
];
let ix = Instruction::new_with_bytes(
program_id,
&claim_rewards_discriminator,
accounts,
);
// Succeeds if the program does not verify recipient == victim
// VULNERABLE: No recipient verification
use anchor_lang::prelude::*;
#[program]
mod vulnerable_rewards {
pub fn claim_rewards(ctx: Context<ClaimRewards>) -> Result<()> {
let rewards_account = &mut ctx.accounts.rewards_pool;
let reward_amount = rewards_account.pending_rewards;
// VULNERABLE: No check that claimant == authorized_beneficiary!
**ctx.accounts.claimant.try_borrow_mut_lamports()? += reward_amount;
**rewards_account.to_account_info().try_borrow_mut_lamports()? -= reward_amount;
rewards_account.pending_rewards = 0;
Ok(())
}
}
// ATTACK: Call claim_rewards with attacker's account as claimant
// All pending_rewards transferred to attacker
// SECURE: Verify recipient authorization
#[program]
mod secure_rewards {
pub fn claim_rewards(ctx: Context<ClaimRewards>) -> Result<()> {
let rewards_account = &mut ctx.accounts.rewards_pool;
// Verify the claimant is the authorized beneficiary
require!(
ctx.accounts.claimant.key() == rewards_account.beneficiary,
RewardsError::Unauthorized
);
require!(ctx.accounts.claimant.is_signer, RewardsError::MissingSignature);
let reward_amount = rewards_account.pending_rewards;
require!(reward_amount > 0, RewardsError::NoRewardsToClaim);
// Safe lamport transfer
**ctx.accounts.claimant.try_borrow_mut_lamports()? += reward_amount;
**rewards_account.to_account_info().try_borrow_mut_lamports()? -= reward_amount;
rewards_account.pending_rewards = 0;
Ok(())
}
}
#[derive(Accounts)]
pub struct ClaimRewards<'info> {
#[account(mut, has_one = beneficiary)]
pub rewards_pool: Account<'info, RewardsPool>,
/// The authorized beneficiary must sign
pub beneficiary: Signer<'info>,
/// Must match rewards_pool.beneficiary
#[account(mut, address = rewards_pool.beneficiary)]
pub claimant: SystemAccount<'info>,
}
Remediation
- Detector: Lamport Drain Detector
- Remediation Guide: Lamport Drain Remediation
Protect all lamport transfer operations with layered authorization:
-
Recipient verification: Always verify the lamport recipient is the authorized account using
has_one,address, or explicit key comparison. -
Signer requirement: Any instruction that transfers lamports must require the beneficiary to be a signer.
-
Balance check: Before subtracting lamports, verify the account has sufficient balance:
require!(account.lamports >= amount, ErrorCode::InsufficientFunds). -
Use Anchor constraints: The
closeconstraint automatically handles rent reclamation to a specified account:#[account(mut, close = owner)].
// Safe lamport transfer pattern
let source_lamports = source_account.try_borrow_mut_lamports()?;
let dest_lamports = destination_account.try_borrow_mut_lamports()?;
require!(**source_lamports >= amount, ErrorCode::InsufficientFunds);
**source_lamports -= amount;
**dest_lamports += amount;
- Rent minimum: After any lamport transfer, verify the source account retains at least its rent-exempt minimum (or is being explicitly closed).