Skip to main content
Sigvex

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.

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

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.

References