Lamports & Rent
Understanding Solana's economic model is crucial for building cost-effective applications. This chapter covers lamports (Solana's atomic unit) and the rent system that governs account storage.
What Are Lamports?
A lamport is the smallest unit of SOL, named after Leslie Lamport (computer scientist, Turing Award winner, creator of LaTeX and Paxos).
UNIT CONVERSION
═══════════════
1 SOL = 1,000,000,000 lamports (1 billion)
1 lamport = 0.000000001 SOL
Common amounts:
├── 1 SOL = 1,000,000,000 lamports
├── 0.1 SOL = 100,000,000 lamports
├── 0.01 SOL = 10,000,000 lamports
├── 0.001 SOL = 1,000,000 lamports
└── 0.000001 SOL = 1,000 lamports (typical tx fee)
Comparison to other blockchains:
├── 1 SOL = 10^9 lamports
├── 1 ETH = 10^18 wei
└── 1 BTC = 10^8 satoshis
Working with Lamports
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
// Constants
console.log("Lamports per SOL:", LAMPORTS_PER_SOL); // 1_000_000_000
// Conversions
function solToLamports(sol: number): number {
return Math.floor(sol * LAMPORTS_PER_SOL);
}
function lamportsToSol(lamports: number): number {
return lamports / LAMPORTS_PER_SOL;
}
// Examples
const oneSol = solToLamports(1); // 1_000_000_000
const halfSol = solToLamports(0.5); // 500_000_000
const backToSol = lamportsToSol(oneSol); // 1
// Safe handling of large numbers
const balance = BigInt(1_000_000_000_000); // 1000 SOL in lamports
const asNumber = Number(balance) / LAMPORTS_PER_SOL;
// Formatting for display
function formatSol(lamports: number | bigint): string {
const sol = Number(lamports) / LAMPORTS_PER_SOL;
return (
sol.toLocaleString("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 9,
}) + " SOL"
);
}
console.log(formatSol(2_500_000_000)); // "2.50 SOL"
The Rent System
Solana uses rent to manage blockchain storage. Every account must either pay rent or maintain a minimum balance (rent-exempt).
Why Rent Exists
THE STORAGE PROBLEM
═══════════════════
Blockchains have unlimited growth:
├── Users create accounts forever
├── Each account costs validators storage
├── Storage costs add up
└── Eventually unsustainable
Ethereum's approach:
├── No rent (historically)
├── State bloat problem
├── State expiry proposals (complex)
Solana's approach:
├── Rent: Pay per byte per epoch
├── Rent-exemption: Deposit covers "forever"
├── Garbage collection: Accounts can be closed
└── Economic incentive to clean up
Rent Calculation
import { Connection } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
// Current rent rate
// ~0.00000348 SOL per byte per year (as of 2024)
// Or ~6.96 lamports per byte per epoch
// Get minimum balance for rent exemption
async function getRentExemption(dataSize: number): Promise<number> {
return await connection.getMinimumBalanceForRentExemption(dataSize);
}
// Examples
const sizes = [0, 100, 1000, 10000, 100000];
for (const size of sizes) {
const lamports = await getRentExemption(size);
console.log(`${size} bytes: ${lamports} lamports (${lamports / 1e9} SOL)`);
}
/*
Example output:
0 bytes: 890880 lamports (0.00089088 SOL)
100 bytes: 1447680 lamports (0.00144768 SOL)
1000 bytes: 7986240 lamports (0.00798624 SOL)
10000 bytes: 72385920 lamports (0.07238592 SOL)
100000 bytes: 718080000 lamports (0.71808 SOL)
*/
Rent Formula
RENT-EXEMPT MINIMUM FORMULA
═══════════════════════════
minimum_balance = (account_size + 128) × rent_rate × exemption_threshold
Where:
├── account_size: Bytes of data
├── 128: Account metadata overhead
├── rent_rate: Lamports per byte per epoch
└── exemption_threshold: ~2 years worth of rent
Current values (approximate):
├── rent_rate: ~3.48 lamports per byte per epoch
├── exemption_threshold: ~2 years = ~730 epochs
├── Per byte: ~2540 lamports (~5 lamports × 730 × factor)
Simplified:
minimum ≈ (data_size + 128) × 6960 lamports
Rent Exemption
Most Solana accounts are rent-exempt, meaning they have enough lamports to cover 2+ years of rent.
// Check if account is rent-exempt
async function isRentExempt(
connection: Connection,
address: PublicKey,
): Promise<{
isExempt: boolean;
balance: number;
required: number;
deficit: number;
}> {
const account = await connection.getAccountInfo(address);
if (!account) {
throw new Error("Account not found");
}
const required = await connection.getMinimumBalanceForRentExemption(
account.data.length,
);
const isExempt = account.lamports >= required;
const deficit = isExempt ? 0 : required - account.lamports;
return {
isExempt,
balance: account.lamports,
required,
deficit,
};
}
// Example usage
const result = await isRentExempt(connection, myAccountAddress);
if (!result.isExempt) {
console.log(`Account needs ${result.deficit} more lamports`);
}
What Happens Without Rent Exemption
RENT COLLECTION SCENARIO (RARE)
═══════════════════════════════
Epoch 100: Account created with 500,000 lamports (not enough)
Data size: 100 bytes
Rent per epoch: ~700 lamports
Epoch 101: Rent collected
Balance: 500,000 - 700 = 499,300 lamports
Epoch 200: After 100 epochs
Balance: 500,000 - 70,000 = 430,000 lamports
Epoch 714: Balance reaches zero
Account is GARBAGE COLLECTED
Data is LOST forever
PREVENTION:
├── Always make accounts rent-exempt
├── Check minimum before creating
└── Add buffer for safety
Creating Rent-Exempt Accounts
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
async function createRentExemptAccount(
connection: Connection,
payer: Keypair,
space: number,
programId: PublicKey,
): Promise<Keypair> {
// 1. Calculate required lamports
const lamports = await connection.getMinimumBalanceForRentExemption(space);
// 2. Generate new account keypair
const newAccount = Keypair.generate();
// 3. Create instruction
const createAccountIx = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: newAccount.publicKey,
lamports,
space,
programId,
});
// 4. Send transaction
const transaction = new Transaction().add(createAccountIx);
await sendAndConfirmTransaction(connection, transaction, [payer, newAccount]);
console.log(`Created account: ${newAccount.publicKey.toBase58()}`);
console.log(`Allocated: ${space} bytes`);
console.log(`Deposited: ${lamports} lamports (${lamports / 1e9} SOL)`);
return newAccount;
}
Closing Accounts (Recovering Rent)
When you no longer need an account, you can close it and recover the lamports:
// Closing an account recovers all lamports
async function closeAccount(
connection: Connection,
account: PublicKey,
destination: PublicKey,
owner: Keypair,
programId: PublicKey,
): Promise<string> {
// The program must implement close functionality
// This is a conceptual example - actual implementation varies by program
const closeIx = new TransactionInstruction({
keys: [
{ pubkey: account, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: owner.publicKey, isSigner: true, isWritable: false },
],
programId,
data: Buffer.from([
/* close instruction discriminator */
]),
});
const transaction = new Transaction().add(closeIx);
return await sendAndConfirmTransaction(connection, transaction, [owner]);
}
Anchor Close Pattern
// In Anchor, closing is straightforward
#[derive(Accounts)]
pub struct CloseAccount<'info> {
#[account(
mut,
close = destination, // Close and send lamports here
has_one = authority, // Verify owner
)]
pub account_to_close: Account<'info, MyData>,
/// CHECK: Receives lamports
#[account(mut)]
pub destination: AccountInfo<'info>,
pub authority: Signer<'info>,
}
// When this instruction runs:
// 1. Account data is zeroed
// 2. All lamports transferred to destination
// 3. Account effectively deleted (will be garbage collected)
Cost Optimization Strategies
1. Right-Size Accounts
// BAD: Over-allocating
const wastefulAccount = await createAccount({
space: 10000, // 10KB when you only need 100 bytes
// Costs: ~0.07 SOL
});
// GOOD: Exact sizing
interface MyData {
counter: bigint; // 8 bytes
owner: PublicKey; // 32 bytes
active: boolean; // 1 byte
}
const EXACT_SIZE = 8 + 32 + 1; // 41 bytes
// Costs: ~0.001 SOL
// Even better with discriminator (Anchor adds 8-byte discriminator)
const ANCHOR_SIZE = 8 + 41; // 49 bytes
2. Reuse Accounts
// Instead of creating new accounts for each operation,
// reuse accounts by resetting their state
pub fn reset_account(ctx: Context<ResetAccount>) -> Result<()> {
let account = &mut ctx.accounts.my_account;
// Reset to initial state instead of closing and recreating
account.counter = 0;
account.data = [0u8; 32];
account.active = false;
Ok(())
}
3. Account Compression
For large datasets, consider compression:
import * as zlib from "zlib";
// Compress before storing
function compressData(data: Buffer): Buffer {
return zlib.gzipSync(data);
}
// Decompress when reading
function decompressData(compressed: Buffer): Buffer {
return zlib.gunzipSync(compressed);
}
// Note: Adds CPU cost, reduces storage cost
// Good for large, infrequently accessed data
4. Off-Chain Storage
// Store large data off-chain, reference on-chain
interface OnChainReference {
// Store hash of data on-chain (32 bytes)
dataHash: Uint8Array;
// Store pointer to off-chain location
arweaveId: string; // Or IPFS CID, etc.
// Timestamp for verification
updatedAt: bigint;
}
// On-chain: ~100 bytes
// Off-chain: Unlimited
// Verification
async function verifyOffChainData(
onChainRef: OnChainReference,
offChainData: Buffer,
): Promise<boolean> {
const hash = sha256(offChainData);
return Buffer.compare(hash, Buffer.from(onChainRef.dataHash)) === 0;
}
Transaction Fees vs Rent
Don't confuse transaction fees with rent:
TRANSACTION FEES
═══════════════
├── Paid per transaction
├── Based on signatures (5000 lamports per sig)
├── Plus priority fee (optional)
├── Covers computation
└── 50% burned, 50% to validator
RENT
════
├── Paid for storage space
├── Based on bytes stored
├── One-time deposit (rent-exempt)
├── Recoverable when closing
└── Prevents state bloat
EXAMPLE TRANSACTION:
├── Base fee: 5000 lamports (0.000005 SOL)
├── Priority fee: 10000 lamports (optional)
├── Rent for new account: 1447680 lamports
└── Total: ~1,462,680 lamports (~0.0015 SOL)
Monitoring Account Economics
// Comprehensive account cost analysis
async function analyzeAccountCosts(
connection: Connection,
address: PublicKey,
): Promise<{
address: string;
size: number;
balance: number;
rentExemptMin: number;
isExempt: boolean;
storageEfficiency: number;
estimatedAnnualRent: number;
}> {
const account = await connection.getAccountInfo(address);
if (!account) {
throw new Error("Account not found");
}
const rentExemptMin = await connection.getMinimumBalanceForRentExemption(
account.data.length,
);
// Calculate storage efficiency (actual data vs zeros)
const nonZeroBytes = account.data.filter((b) => b !== 0).length;
const storageEfficiency = nonZeroBytes / account.data.length;
// Estimate annual rent if not exempt
const epochsPerYear = 365 * 2; // ~730 epochs
const rentPerEpoch = account.data.length * 3.48; // lamports
const estimatedAnnualRent = rentPerEpoch * epochsPerYear;
return {
address: address.toBase58(),
size: account.data.length,
balance: account.lamports,
rentExemptMin,
isExempt: account.lamports >= rentExemptMin,
storageEfficiency,
estimatedAnnualRent,
};
}
Key Takeaways
- 1 SOL = 1 billion lamports - always work in lamports for precision
- Rent-exemption is standard - deposit ~2 years of rent upfront
- Minimum balance scales with size - ~0.007 SOL per KB
- Closing accounts recovers lamports - always clean up unused accounts
- Right-size accounts - don't over-allocate storage
Next: Owner & Authority - Understanding account permissions and control.