Reading & Writing Account Data
Account data lives in a runtime-managed byte buffer. Programs borrow immutably or mutably, parse into types, mutate, and write back with Borsh or zero-copy updates.
Recipe
let mut data = account.try_borrow_mut_data()?;
let state = State::deserialize(&mut &data[..])?;When to reach for this:
- Updating counters, config, or user state.
- Reading SPL token account fields (via spl-token layout).
- Migrating account layouts in upgrade instructions.
Working Example
pub fn increment(account: &AccountInfo) -> ProgramResult {
if account.data_len() < State::LEN { return Err(ProgramError::InvalidAccountData); }
let mut data = account.try_borrow_mut_data()?;
let mut state = State::deserialize(&mut &data.as_ref()).map_err(|_| ProgramError::InvalidAccountData)?;
state.count = state.count.checked_add(1).ok_or(ProgramError::InvalidArgument)?;
state.serialize(&mut &mut data.as_mut()).map_err(|_| ProgramError::AccountDataTooSmall)?;
Ok(())
}What this demonstrates:
- Length guard before deserialize.
try_borrow_mut_datafor writes.- Map serialize errors to
AccountDataTooSmall.
Deep Dive
Borrow Rules
- One mutable borrow OR many immutable.
- Drop borrows before CPI on same account.
Partial Updates
Zero-copy allows field mutation without full serialize.
Rust Notes
// Prefer &mut [u8] subslices for discriminator + body.Gotchas
- Borrow across CPI - Runtime error/panic.. Fix: End borrow before invoke.
- Stale layout - Deserialize old struct on new program.. Fix: Version byte + migration.
- Token account confusion - Treat token layout separately from custom State.. Fix: Use spl-token pack/unpack.
- Resize without realloc - Serialize fails if account too small.. Fix: CPI realloc instruction first.
- Unaligned zero-copy - UB on bad casts.. Fix: Check size and alignment.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| bytemuck field update | Hot path counters | Variable strings |
| Anchor Account::try_from | Auto owner/discriminator | Native |
| Read-only views | Pinocchio | Ecosystem |
FAQs
try_borrow vs borrow?
Use try_ variants in programs - fail gracefully.
Who can write?
Account owner program only.
Read others' accounts?
Yes if passed in transaction - validate owner.
Empty data?
Uninitialized - reject or init path.
Max write size?
Account allocated space.
Concurrent txs?
Runtime serializes per account per tx.
Log data bytes?
Avoid - leaks user info.
Close clears data?
Zero lamports after close - zero data first.
realloc?
See realloc article.
Rent after grow?
More bytes need more lamports.
Read sysvar data?
Use dedicated sysvar types.
Tests?
LiteSVM with account fixtures.
Related
- Borsh Structs - Layouts
- realloc & Growing Accounts - Resize
- Account Discriminators - Typing
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.