Skip to content

Latest commit

 

History

History

README.md

Missing Rent Exemption Check - Security Example

Vulnerability Overview

Severity: HIGH

This example demonstrates the Missing Rent Exemption Check vulnerability, where withdrawing lamports without verifying the remaining balance stays above rent-exempt minimum can cause accounts to be garbage collected.

The Problem

Solana accounts must maintain a minimum balance (rent-exempt threshold) to persist on-chain. If an account's lamports fall below this threshold:

  1. The account becomes eligible for garbage collection
  2. The runtime may purge the account
  3. All account data is permanently lost
  4. Any remaining lamports are lost

Real-World Impact

This vulnerability can lead to:

  • Lost funds - Remaining lamports in the account are gone
  • Lost data - All account state is wiped
  • Protocol breakage - Critical accounts disappear
  • User confusion - Accounts vanish unexpectedly

What You'll Learn

  1. How Solana's rent system works
  2. Why rent-exemption is important
  3. How to calculate rent-exempt minimum
  4. Properly checking before withdrawing lamports

Program Structure

  1. initialize_vault - Creates a vault account
  2. deposit - Deposits lamports into the vault
  3. withdraw_vulnerable - Withdraws without rent check (VULNERABLE)
  4. withdraw_secure - Withdraws with rent check (SECURE)

The Vulnerability: Unchecked Withdrawal

Vulnerable Code

Located in: src/instructions/withdraw_vulnerable.rs

pub fn handler(ctx: Context<WithdrawVulnerable>, amount: u64) -> Result<()> {
    let vault_info = ctx.accounts.vault.to_account_info();
    let vault_balance = vault_info.lamports();

    // ⚠️ VULNERABLE: Only checks if we have enough lamports
    // Does NOT check if remaining balance stays rent-exempt!
    require!(
        vault_balance >= amount,
        ErrorCode::InsufficientFunds
    );

    // Transfer lamports out
    **vault_info.try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.owner.try_borrow_mut_lamports()? += amount;

    // ⚠️ If remaining < rent_exempt_minimum, account could be PURGED!
    Ok(())
}

The Attack Scenario

ACCOUNT PURGE SCENARIO
======================

Initial State:
┌─────────────────────────────────────────┐
│ Vault Account                           │
│ ├─ lamports: 1,500,000 (0.0015 SOL)    │
│ ├─ data: [owner, bump, ...]            │
│ └─ rent_exempt_min: 890,880            │
└─────────────────────────────────────────┘

User calls withdraw_vulnerable(1,000,000):
┌─────────────────────────────────────────┐
│ Check: 1,500,000 >= 1,000,000? ✓        │
│ Withdraw executes...                    │
│                                         │
│ NEW STATE:                              │
│ ├─ lamports: 500,000                   │
│ ├─ rent_exempt_min: 890,880            │
│ └─ 500,000 < 890,880 ❌                 │
└─────────────────────────────────────────┘
              │
              │ End of transaction / epoch boundary
              ▼
┌─────────────────────────────────────────┐
│ Runtime Garbage Collection:             │
│                                         │
│ Account lamports < rent exempt?         │
│ 500,000 < 890,880? YES                  │
│                                         │
│ ⚠️ ACCOUNT PURGED!                      │
│ ├─ 500,000 lamports: LOST              │
│ └─ All account data: LOST              │
└─────────────────────────────────────────┘

The Fix: Rent-Exempt Validation

Secure Code

Located in: src/instructions/withdraw_secure.rs

pub fn handler(ctx: Context<WithdrawSecure>, amount: u64) -> Result<()> {
    let vault_info = ctx.accounts.vault.to_account_info();
    let vault_balance = vault_info.lamports();

    // ✅ SECURE: Calculate rent-exempt minimum for this account
    let rent = Rent::get()?;
    let rent_exempt_minimum = rent.minimum_balance(vault_info.data_len());

    // ✅ SECURE: Calculate maximum withdrawable amount
    let max_withdrawable = vault_balance
        .checked_sub(rent_exempt_minimum)
        .unwrap_or(0);

    // ✅ SECURE: Check that withdrawal won't violate rent exemption
    require!(
        amount <= max_withdrawable,
        ErrorCode::WouldViolateRentExemption
    );

    // Safe to transfer
    **vault_info.try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.owner.try_borrow_mut_lamports()? += amount;

    Ok(())
}

Why This Works

Aspect Vulnerable Secure
Checks balance Only total Against rent minimum
Allows draining Yes No (protects minimum)
Account persists Maybe not Always
Data safety At risk Protected

Understanding Rent Exemption

Calculating Rent-Exempt Minimum

// Get the Rent sysvar
let rent = Rent::get()?;

// Calculate minimum for account's data size
let minimum = rent.minimum_balance(account.data_len());

// Typical values (as of 2024):
// - Base: ~890,880 lamports for 0 bytes
// - Per byte: ~6,960 lamports
// - 100 byte account: ~1,586,880 lamports

Safe Withdrawal Pattern

// ✅ Always calculate max withdrawable
let max_withdrawable = current_balance
    .saturating_sub(rent_exempt_minimum);

// ✅ Validate before withdrawing
require!(amount <= max_withdrawable, Error);

// ✅ Or provide a "withdraw_max" function
pub fn withdraw_max(ctx: Context<Withdraw>) -> Result<()> {
    let rent = Rent::get()?;
    let minimum = rent.minimum_balance(data_len);
    let withdrawable = balance.saturating_sub(minimum);
    // Transfer withdrawable amount
}

Key Differences

Aspect Vulnerable Secure
Rent check None Rent::get()?.minimum_balance()
Max withdrawal Full balance Balance - rent_exempt
Account fate May be purged Always persists
Data safety At risk Protected

Edge Cases to Consider

1. Close Account Properly

If you want to fully drain an account, use Anchor's close constraint:

#[account(mut, close = recipient)]
pub account_to_close: Account<'info, MyAccount>,

2. Account Size Changes

If account size can change, recalculate rent minimum:

// After resizing
account.realloc(new_size, false)?;
let new_minimum = rent.minimum_balance(new_size);

3. Rent-Paying Accounts (Deprecated)

Modern Solana requires rent-exemption for all accounts. The rent-paying model is deprecated.

Key Takeaways

DON'T

  • Withdraw lamports without checking rent exemption
  • Assume accounts will persist after draining
  • Forget that remaining lamports can be lost

DO

  • Always calculate rent.minimum_balance(data_len)
  • Check withdrawal <= balance - rent_minimum
  • Use Anchor's close constraint to fully drain accounts
  • Provide clear error messages about rent limits

Building This Example

cd 10-missing-rent-check
anchor build

Additional Resources


Remember: Always check rent exemption before withdrawing lamports. Accounts below the threshold can be garbage collected, losing both data and remaining funds!