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.
Recommended Fix
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.
reallocthat 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 + bufferso 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.