Skip to content

Accounts

Accounts are the fundamental unit of state on the SVM. Every piece of persistent data — program bytecode, token balances, user records, configuration — lives inside an account. There are no implicit storage slots or contract-scoped databases; if data exists on-chain, it exists as an account.

Upstream-compatible

The account model on Zink is identical to Solana mainnet. Accounts have the same structure, the same ownership rules, and the same rent mechanics. Existing code that reads or writes accounts requires no changes.

Account structure

Every account on the network is a contiguous region of bytes associated with a public key. The runtime tracks five fields per account:

FieldTypeDescription
keyPubkey (32 bytes)The address of the account — its unique identifier on the network
lamportsu64The account's balance in lamports (1 SOL = 1,000,000,000 lamports)
dataVec<u8>An arbitrary byte buffer that holds the account's state
ownerPubkey (32 bytes)The program ID that owns this account and is allowed to modify its data
executableboolWhether this account contains executable program code

Additionally, the runtime tracks a rent_epoch field used internally for rent collection bookkeeping.

┌──────────────────────────────────────────────┐
│  Account (on-chain)                          │
├──────────────┬───────────────────────────────┤
│ key          │ 8Xkco...9vBn (32 bytes)      │
│ lamports     │ 1_000_000_000                 │
│ data         │ [u8; N]                       │
│ owner        │ TokenkegQf...  (program ID)   │
│ executable   │ false                         │
│ rent_epoch   │ 365                           │
└──────────────┴───────────────────────────────┘

Ownership rules

Every account has exactly one owner — a program ID. The ownership model enforces two critical invariants:

  1. Only the owner program can modify an account's data bytes. Any program can read any account, but write access is exclusive to the owner.
  2. Only the owner program can debit an account's lamports. Any program can credit lamports to any account (increase the balance), but only the owner can debit (decrease the balance).

When a new account is created via the System Program, its owner is initially set to the System Program (11111111111111111111111111111111). Programs typically reassign ownership to themselves as part of account initialization by calling the System Program's create_account instruction, which atomically allocates space, transfers lamports, and assigns the new owner.

Zink recommendation

Programs that expect other applications to compose with their state should follow established account layout conventions to simplify indexing and integration. Consistent discriminator and serialization patterns make it possible for ecosystem tools to decode accounts uniformly.

Rent and rent-exemption

Accounts consume validator memory, and the network charges rent for that storage. In practice, all accounts should be made rent-exempt by holding a minimum lamport balance proportional to their data size.

The minimum balance for rent-exemption is:

minimum_balance = (128 + data_length) × rent_rate_per_byte_year × 2

The factor of 128 accounts for the fixed overhead of the account metadata. The multiplier of 2 means the account must hold two years' worth of rent to be exempt.

You can query the exact value from the runtime:

bash
# CLI
solana rent <DATA_SIZE_IN_BYTES>
rust
// On-chain (inside a program)
let min_balance = Rent::get()?.minimum_balance(data_len);

Zink-specific

Rent parameters on Zink clusters are configured independently. Query the cluster directly rather than assuming Solana mainnet defaults. Use solana rent <size> --url <ZINK_RPC> to get the current value.

Since the introduction of rent-exemption enforcement, accounts that fall below the minimum balance are rejected at transaction time. In practice this means every account you create must be funded to at least the rent-exempt minimum.

Creating accounts

The System Program is the only program that can create new accounts. The standard flow is:

  1. Determine the data size your account needs.
  2. Query Rent::get()?.minimum_balance(data_size) for the required lamports.
  3. Issue a create_account instruction to the System Program specifying the payer, new account key, lamports, space, and owner program.

In Starframe, Init plus Create handles the account creation flow while keeping the payer, seeds, and account type explicit:

rust
#[derive(AccountSet, Debug)]
pub struct InitializeAccounts {
    #[validate(funder)]
    pub authority: Signer<Mut<SystemAccount>>,

    #[validate(arg = (
        Create(()),
        Seeds(MyDataSeeds { authority: *self.authority.pubkey() }),
    ))]
    pub my_account: Init<Seeded<Account<MyData>>>,

    pub system_program: Program<System>,
}

Size calculations are still your responsibility. Reserve enough bytes for the data layout your program actually writes, including any discriminator or custom header you choose to include.

Account size limits

  • Maximum account data size: 10 MiB (10,485,760 bytes).
  • Accounts can be resized after creation using realloc (in native programs) or realloc helpers or explicit System Program allocation flows, up to the 10 MiB cap. Each realloc may require additional lamports to maintain rent-exemption.
  • The size of an account can also be decreased, refunding the excess lamports to a designated account.

Data serialization

The runtime treats the data field as an opaque byte buffer. Programs choose their own serialization format:

ApproachDescription
BorshThe default in the Solana ecosystem. Deterministic, compact, and well-supported in Rust and JS.
Zero-copyMaps a struct directly onto the account's byte buffer with no (de)serialization cost. Useful for large, frequently-accessed accounts. Starframe supports this with #[zero_copy(pod)] plus fixed-size account types.
CustomPrograms can use any format (MessagePack, Protobuf, raw byte packing) as long as they handle serialization themselves.

The relationship between accounts and programs

A program is itself an account — one with the executable flag set to true. The program's bytecode is stored in a separate ProgramData account managed by the BPF Loader. When you deploy a program, the loader creates both the executable account (the program ID) and the ProgramData account that holds the actual sBPF bytecode.

Every non-executable account is owned by exactly one program. That program defines the rules for reading and writing the account's data. This is why you pass accounts into program instructions: the runtime checks at the boundary that the instruction's program is the owner of every account it tries to modify.

For more on programs themselves, see Programs. For how programs derive deterministic account addresses, see PDAs.

Zink is a general-purpose SVM network for programs, operators, and bridge integrations.