Missing Rent Check Remediation
Overview
Related Detector: Missing Owner Check
Solana accounts must maintain a minimum lamport balance to be considered rent-exempt. Accounts below this threshold are candidates for garbage collection by the runtime, causing unexpected failures when a program tries to read data from a collected account. The vulnerability arises in two forms: creating accounts with an insufficient or user-controlled lamport allocation, and allowing withdrawals that reduce an account’s lamports below the minimum balance. The fix is to compute the required minimum via Rent::get()?.minimum_balance(data_len) and enforce it at both creation and withdrawal boundaries.
Recommended Fix
Before (Vulnerable)
use anchor_lang::prelude::*;
use anchor_lang::system_program;
#[program]
mod vulnerable_protocol {
pub fn create_user_account(
ctx: Context<CreateUserAccount>,
lamports: u64, // VULNERABLE: caller-controlled amount
) -> Result<()> {
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 may supply 0 or any sub-minimum value
UserData::LEN as u64,
ctx.program_id,
)?;
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// VULNERABLE: no check that remaining lamports meet rent-exempt minimum
**ctx.accounts.vault.to_account_info().try_borrow_mut_lamports()? -= amount;
**ctx.accounts.recipient.try_borrow_mut_lamports()? += amount;
Ok(())
}
}
After (Fixed)
use anchor_lang::prelude::*;
use anchor_lang::system_program;
#[program]
mod secure_protocol {
pub fn create_user_account(ctx: Context<CreateUserAccount>) -> Result<()> {
// FIXED: always compute the minimum — never accept a caller-supplied amount
let rent = Rent::get()?;
let space = UserData::LEN;
let minimum_lamports = rent.minimum_balance(space);
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, // Computed — not user-controlled
space as u64,
ctx.program_id,
)?;
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
let vault_info = ctx.accounts.vault.to_account_info();
let current_lamports = vault_info.lamports();
// FIXED: compute the remaining balance and reject if below minimum
let remaining = current_lamports
.checked_sub(amount)
.ok_or(error!(ErrorCode::InsufficientFunds))?;
let rent = Rent::get()?;
let minimum = rent.minimum_balance(vault_info.data_len());
require!(
remaining >= minimum,
ErrorCode::WouldViolateRentExemption
);
**vault_info.try_borrow_mut_lamports()? -= amount;
**ctx.accounts.recipient.try_borrow_mut_lamports()? += amount;
Ok(())
}
}
The fix removes the user-controlled lamports parameter entirely and computes the required minimum from the account size at call time. Withdrawal logic checks that the post-withdrawal balance remains at or above the rent-exempt threshold before modifying any state.
Alternative Mitigations
1. Anchor init constraint (recommended for Anchor users)
the account-validation framework’s init constraint computes the minimum balance, creates the account via CPI, and verifies rent exemption automatically — no manual calculation is needed:
use anchor_lang::prelude::*;
#[derive(Accounts)]
pub struct CreateUserAccount<'info> {
#[account(
init,
payer = payer,
space = 8 + UserData::LEN, // 8 bytes for Anchor discriminator
// an account-validation framework automatically:
// 1. Calls Rent::get().minimum_balance(space)
// 2. Creates the account with that exact lamport amount
// 3. Verifies the account is rent-exempt after creation
)]
pub new_account: Account<'info, UserData>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
2. Validate existing accounts on entry
When accepting an existing account that must remain rent-exempt throughout the instruction, validate its current balance at the top of the handler:
fn assert_rent_exempt(account: &AccountInfo) -> Result<()> {
let rent = Rent::get()?;
let minimum = rent.minimum_balance(account.data_len());
require!(
account.lamports() >= minimum,
ErrorCode::AccountNotRentExempt
);
Ok(())
}
pub fn process_deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
assert_rent_exempt(&ctx.accounts.user_vault.to_account_info())?;
// ... proceed with deposit logic
Ok(())
}
3. Rent::is_exempt helper
The Rent struct provides an is_exempt method that combines the minimum-balance calculation and the comparison in a single call:
pub fn validate_rent_exemption(account: &AccountInfo) -> Result<()> {
let rent = Rent::get()?;
let data_len = account.data_len();
if !rent.is_exempt(account.lamports(), data_len) {
msg!(
"Account {} has {} lamports; minimum for {} bytes is {}",
account.key,
account.lamports(),
data_len,
rent.minimum_balance(data_len)
);
return Err(ErrorCode::AccountNotRentExempt.into());
}
Ok(())
}
Common Mistakes
Mistake 1: Hardcoding a lamport amount instead of computing it
// WRONG: hardcoded value may be correct today but invalid after a rent-rate change
system_program::create_account(
cpi_ctx,
890_880, // Hardcoded — depends on account size and current rent rate
account_size as u64,
ctx.program_id,
)?;
Always call rent.minimum_balance(space) dynamically. The minimum balance is a function of account data size, not a constant.
Mistake 2: Checking rent exemption after the lamport transfer
// WRONG: the account may already be below the threshold when the check runs
**vault.try_borrow_mut_lamports()? -= amount;
let rent = Rent::get()?;
require!(
rent.is_exempt(vault.lamports(), vault.data_len()),
ErrorCode::RentViolation // Too late — state is already modified
);
Always verify the post-operation balance before making any lamport changes.
Mistake 3: Forgetting the account-validation framework discriminator in the space calculation
// WRONG: missing 8 bytes for the account-validation framework account discriminator
#[account(
init,
payer = payer,
space = std::mem::size_of::<UserData>(), // Off by 8!
)]
pub new_account: Account<'info, UserData>,
// CORRECT:
space = 8 + std::mem::size_of::<UserData>(),
An account created with too little space will also be too small for the required minimum lamport balance.