Skip to main content
Sigvex

Rent Collection Exploit Remediation

How to keep accounts rent-exempt through withdrawals and reallocations by computing minimum balances dynamically and enforcing them on every lamport movement.

Rent Collection Exploit Remediation

Overview

Related Detector: Rent Collection Exploit

An account that drops below its rent-exempt minimum is subject to garbage collection, taking its data — position records, escrow state, authority configuration — with it. Programs create this exposure in two ways: withdrawing lamports without checking the remaining balance against the rent-exempt threshold, and hardcoding a threshold that does not match the account’s actual data length. The fix is to compute the minimum from the Rent sysvar and the account’s current size, and to enforce it on every code path that removes lamports or grows the account.

Before (Vulnerable)

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    // WRONG: no rent-exemption check — the account can be drained
    // to a balance the runtime will garbage-collect
    **ctx.accounts.vault.try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.user.try_borrow_mut_lamports()? += amount;
    Ok(())
}

After (Fixed)

pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let vault = &ctx.accounts.vault;
    let rent = Rent::get()?;
    let min_balance = rent.minimum_balance(vault.data_len());

    let remaining = vault
        .lamports()
        .checked_sub(amount)
        .ok_or(ErrorCode::InsufficientFunds)?;
    require!(remaining >= min_balance, ErrorCode::BelowRentExempt);

    **vault.try_borrow_mut_lamports()? -= amount;
    **ctx.accounts.user.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Rent::get() reads the live sysvar and minimum_balance(data_len) scales with the account’s actual size, so the check stays correct if rent parameters change or the account is reallocated.

Alternative Mitigations

  • Close instead of drain. If the intent is to empty the account, close it properly: transfer all lamports, zero the data, and let the runtime reclaim it — rather than leaving a partially funded account below threshold.
  • Top up on realloc. realloc that grows an account raises its rent-exempt minimum. Recompute the minimum after the new size and transfer the difference from the payer in the same instruction.
  • Reserve headroom. For accounts that receive frequent small withdrawals, enforce min_balance + buffer so that fee-level fluctuations never bring the balance to the edge.

Common Mistakes

Mistake: Hardcoding the Threshold

// WRONG: only correct for one specific data length, and only
// under current rent parameters
const RENT_EXEMPT: u64 = 890_880;
require!(vault.lamports() - amount >= RENT_EXEMPT, BelowRentExempt);

Mistake: Checking Before the Size Changes

require!(vault.lamports() >= rent.minimum_balance(vault.data_len()), _);
vault.realloc(new_len, false)?;  // WRONG: minimum just increased

Order matters: the check must use the size the account will have when the instruction completes.

References