Missing Rent Check Exploit Generator
Sigvex exploit generator that validates missing rent-exempt check vulnerabilities in Solana programs where accounts with insufficient lamport balances may be garbage-collected by the runtime, causing unexpected program failures.
Missing Rent Check Exploit Generator
Overview
The missing rent check exploit generator validates findings from the missing-rent-check detector (severity score 50/100) by identifying Solana programs that create or accept accounts without verifying they meet the rent-exempt minimum lamport balance. Accounts below the rent-exempt threshold are subject to periodic lamport deduction and eventual garbage collection by the runtime, which can break protocol invariants.
Solana’s rent mechanism historically deducted lamports from non-exempt accounts each epoch. While rent collection is largely deprecated on mainnet, the rent-exempt status of accounts remains critical for program correctness: accounts must maintain a minimum lamport balance (determined by Rent::minimum_balance(data_len)) to be rent-exempt. Programs that create accounts without ensuring rent exemption, or that allow lamport transfers that reduce an account below the threshold, can have those accounts unexpectedly garbage-collected or rendered invalid.
Severity score: 50/100 (Medium).
Note: Exploit generation in Sigvex is for vulnerability validation purposes only.
Attack Scenario
Account creation below rent minimum:
- A program creates a data account for a user with a fixed lamport allocation.
- The allocation is hardcoded as
0.001 SOLbut the required rent-exempt minimum for the account size is0.002 SOL. - The account is created at
0.001 SOL— below the threshold. - The runtime garbage-collects the account after a period.
- The program attempts to read the user’s data and fails with “account not found.”
- User funds associated with the account (stored as a reference) are inaccessible.
Force below rent threshold:
- A program has a
withdrawinstruction that allows withdrawing all but 1 lamport. - The minimum rent-exempt balance for the account size is 890,880 lamports (0.00089 SOL).
- An attacker withdraws until the account has only 1 lamport — far below the threshold.
- The runtime may garbage-collect the account on the next epoch.
- Any data or authority stored in the account is lost.
New account validation bypass:
- A protocol creates program accounts via CPI to the System Program.
- The program passes a user-controlled
lamportsvalue to thecreate_accountCPI. - The user specifies
lamports = 0. - The account is created with 0 lamports — immediately below the rent threshold.
- The account is garbage-collected before any useful program logic can use it.
Exploit Mechanics
The engine maps the missing-rent-check detector to the rent check exploit class. Assessment is performed via static analysis of account creation and lamport management code paths. Dynamic simulation is not applicable because below-minimum lamport balances do not cause immediate runtime errors — accounts are garbage collected asynchronously by the Solana runtime rather than failing at instruction time.
Rent exempt minimum calculation:
- Base overhead: 128 bytes
- Data bytes: variable (account data size)
- Lamports per byte-year: 3,480 (at current rates)
- For a 200-byte account:
minimum_balance ≈ 1,428 * 200 + 128 overhead ≈ 1,461,600 lamports ≈ 0.00146 SOL
// VULNERABLE: Hardcoded or user-controlled lamport amount for new account
use anchor_lang::prelude::*;
use anchor_lang::system_program;
#[program]
mod vulnerable_protocol {
pub fn create_user_account(ctx: Context<CreateUserAccount>, lamports: u64) -> Result<()> {
// VULNERABLE: lamports is user-controlled, may be below rent threshold!
system_program::create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.new_account.to_account_info(),
},
),
lamports, // Attacker controls this!
UserAccount::LEN as u64,
ctx.program_id,
)?;
Ok(())
}
}
// SECURE: Always use the computed minimum balance
#[program]
mod secure_protocol {
pub fn create_user_account(ctx: Context<CreateUserAccountSafe>) -> Result<()> {
// Compute the minimum rent-exempt balance for this account size
let rent = Rent::get()?;
let space = UserAccount::LEN;
let minimum_lamports = rent.minimum_balance(space);
// Use the computed minimum — never accept user-controlled amounts
system_program::create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.new_account.to_account_info(),
},
),
minimum_lamports,
space as u64,
ctx.program_id,
)?;
Ok(())
}
}
// Anchor handles rent automatically with init constraint
#[derive(Accounts)]
pub struct CreateUserAccountAnchor<'info> {
#[account(
init,
payer = payer,
space = 8 + UserAccount::LEN, // 8 bytes for discriminator
// Anchor automatically computes and charges minimum rent-exempt balance
)]
pub new_account: Account<'info, UserAccount>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Validate existing account has sufficient lamports
fn verify_rent_exempt(account: &AccountInfo) -> Result<()> {
let rent = Rent::get()?;
let data_len = account.data.borrow().len();
let minimum = rent.minimum_balance(data_len);
if account.lamports() < minimum {
msg!(
"Account {} has {} lamports, needs {} for rent exemption",
account.key,
account.lamports(),
minimum
);
return Err(ErrorCode::AccountNotRentExempt.into());
}
Ok(())
}
Remediation
- Detector: Missing Rent Check Detector
- Remediation Guide: Missing Rent Check Remediation
The simplest remediation is to use Anchor’s init constraint, which automatically handles rent exemption:
// Anchor's init constraint:
// 1. Computes minimum_balance(space)
// 2. Creates the account with exactly that many lamports
// 3. Verifies the account is now rent-exempt
#[account(
init,
payer = payer,
space = 8 + std::mem::size_of::<YourStruct>(),
)]
pub new_account: Account<'info, YourStruct>,
For manual account creation:
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_size);
// Always use required_lamports, never a hardcoded or user-supplied value
For withdrawals, verify the remaining balance meets the threshold:
let new_lamports = account.lamports().checked_sub(withdraw_amount)
.ok_or(error!(ErrorCode::InsufficientFunds))?;
let rent = Rent::get()?;
let minimum = rent.minimum_balance(account.data_len());
if new_lamports < minimum {
return Err(error!(ErrorCode::WouldViolateRentExemption));
}