Missing Rent Check Remediation
How to verify rent exemption when creating or modifying Solana accounts to prevent garbage collection of program-owned accounts.
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)
Anchor’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
// Anchor 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 Anchor discriminator in the space calculation
// WRONG: missing 8 bytes for the Anchor 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.