Blockchain9 min readUpdated

Rust Data Types for Solana: u8, u64, bool, String

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: Rust Data Types for Solana: u8, u64, bool, String

Section 01 · Why Types Matter

Why Rust's type system makes Solana programs safer

Compile time guarantees eliminate an entire class of bugs before your program ever reaches a validator.

Quick answer

What Rust data types does Solana use? Solana programs use u8 for small counters and bump seeds, u64 for all lamport and token amounts, i64 for Unix timestamps, bool for account flags, String for text stored on chain, and Pubkey for all addresses. These six types cover the vast majority of fields in real production programs.

Rust eliminates an entire category of bugs before your program ever reaches a validator. The type checker enforces at compile time that a u8 field never holds a negative number and a Pubkey field never holds a float. No runtime exception, no silent corruption — the program simply does not compile if the types are wrong.

This matters more for Solana than for most other environments. A deployed Solana program processes thousands of transactions per second from users who may be trying to exploit it. A type error that slips through in a dynamically typed language could drain funds. Rust refuses to let it through at all.

The second reason types matter is determinism. Every validator runs the same program on the same inputs and must produce the same result. If a type choice could produce different results on different hardware — as floats can — the network treats the transaction as invalid. Rust's numeric types are defined to be deterministic on all platforms, provided you stay in the integer world.

rust
use solana_program::pubkey::Pubkey;

// A realistic account struct showing common type choices
pub struct TokenMint {
    pub authority: Pubkey,    // 32 bytes — who controls the mint
    pub supply: u64,          // 8 bytes  — total tokens in circulation
    pub decimals: u8,         // 1 byte   — token precision (0-9 typical)
    pub is_initialized: bool, // 1 byte   — was this account set up?
    pub freeze_authority: Option<Pubkey>, // 1 + 32 bytes
}

Section 02 · Integers

Unsigned and signed integers

Solana programs use eight fixed width integer types. Knowing which one to pick prevents overflow bugs and keeps account size predictable.

Unsigned integers hold only non negative values. They are the right default for any numeric field in a Solana account struct. u8 fits in 1 byte and covers 0 to 255 — the right choice for version numbers, bump seeds, and token decimal places. u32 fits in 4 bytes and covers 0 to 4.29 billion, useful for array indices and medium sized counters. u64 fits in 8 bytes and covers 0 to 18.4 quintillion — the type you will reach for on almost every numeric field that matters.

Use u128 (16 bytes) when you need to multiply two u64 values before dividing — the intermediate result can exceed u64::MAX. DeFi protocols use u128 for price accumulators and reward calculations.

Signed integers mirror the unsigned set but allow negative values. i64 is the most common: the Solana Clock sysvar exposes unix_timestamp as i64, so any timestamp field in your account struct should match that type.

Six numbered cards showing Rust types used in Solana: u8, u64, i64, bool, Pubkey, and String with their byte sizes and typical use cases.
The six types that cover almost every field in a real Solana account struct, with their sizes and primary use cases.

Always use checked arithmetic on balance fields

The standard + and - operators wrap silently in release builds. A u64 that overflows does not panic — it wraps to a wrong but valid looking number. Use checked_sub and checked_add for any field that holds a monetary amount. They return Option and force you to handle the overflow case explicitly, turning a silent bug into a proper program error.

rust
// Checked arithmetic — the right way to handle balances
let balance: u64 = 1_000_000_000; // 1 SOL in lamports
let fee: u64     =         5_000; // 5000 lamports base fee

// checked_sub returns None if the result would underflow
let after_fee = balance
    .checked_sub(fee)
    .ok_or(ProgramError::InsufficientFunds)?;

// Saturating — clamp instead of panicking (useful for caps)
let max_supply: u64 = 1_000_000_000_000; // 1 trillion tokens
let new_supply = current_supply.saturating_add(mint_amount);

Section 03 · Boolean

bool in Solana programs

One byte, two states. The right type for account flags and feature toggles.

A bool is exactly 1 byte on chain, serialised as 1 for true and 0 for false. It is the right type for any field that represents a binary state: whether an account has been initialised, whether a feature is active, whether a position is locked.

The is_initialized field is the canonical example. Programs that do not use Anchor's discriminator system check this field before processing an instruction, to prevent initialising the same account twice. Even with Anchor, you will see bool fields for flags like is_paused or is_frozen on token or governance accounts.

One rule to keep in mind: bool is not the right type for a counter, a version, or anything that might grow beyond two states. Use u8 for that. A bool field that you later need to extend to three values requires a schema migration, which is painful on chain.

Section 04 · Float Warning

Why you should never use f32 or f64 in Solana

Floating point is not deterministic across CPU architectures. On a network that requires every validator to agree, that is a fatal flaw.

Never use f32 or f64 in on chain Solana programs

Floating point arithmetic produces results that can differ by a single bit depending on CPU architecture and runtime rounding mode. Validator nodes run on different hardware. If two validators disagree on a calculation result by even one bit, the network rejects the transaction. Use integer math with an explicit scale factor instead — represent $10.25 as the integer 10250 with two implied decimal places.

The problem is not theoretical. IEEE 754 floating point allows compliant implementations to produce slightly different results depending on the processor's handling of denormal numbers and extended precision modes. A calculation that produces 1.00000000000001 on one processor might produce 0.99999999999999 on another. Both results are valid floats. The difference is a single bit.

For Solana, that single bit is catastrophic. The consensus mechanism requires every validator to produce the same output from the same input. A computation that diverges by even one bit between two validators is treated as a fault. Your transaction gets rejected on mainnet.

rust
// Why f32/f64 is dangerous in Solana (do NOT use this pattern)
// pub fn bad_price_calc(amount: f64, price: f64) -> f64 {
//     amount * price  // Different CPUs may round differently!
//                     // Validators can disagree → runtime panic
// }

// The correct approach: integer math with explicit scale
pub fn price_in_lamports(amount_tokens: u64, price_per_token: u64) -> u64 {
    // Both values are already scaled integers (e.g. 1000 = $10.00 with 2 decimals)
    amount_tokens
        .checked_mul(price_per_token)
        .and_then(|v| v.checked_div(100)) // de-scale the price
        .unwrap_or(0)
}

Section 05 · Strings

String and &str on chain

Both types end up as the same bytes on chain. The difference is ownership, and ownership determines which one belongs in an account struct.

String is heap allocated and owned — it carries its data with it wherever it goes and frees it when it goes out of scope. &str is a borrowed reference to a string slice that lives somewhere else, either inside a String or in program memory. Your account structs almost always use String; instruction handler functions often accept &str for parameters where you want to borrow without taking ownership.

On chain, both end up identical: Borsh writes a 4 byte little endian integer containing the string length, followed by the raw UTF-8 bytes. A 5 character string like hello occupies 9 bytes: 4 for the length prefix and 5 for the characters.

Vertical flow diagram showing a Rust String value passing through the Borsh serializer to produce a 9 byte on chain representation with a 4 byte length prefix.
How Borsh serialises a String: a 4 byte little endian length prefix followed by the raw UTF-8 bytes, for a total of 9 bytes for the word hello.

The critical constraint: you must cap string length before allocating the account. On chain, an account's data field has a fixed size set at creation time. A string that grows beyond the initial allocation would require closing and recreating the account, which is expensive. Anchor enforces the limit at the type level with #[max_len(N)]. If a user passes a string longer than N bytes, the instruction fails before any state changes.

For context on how strings and other fields combine inside a struct, the Rust Structs in Solana guide walks through the full space calculation step by step.

Section 06 · Pubkey

Pubkey: Solana's address type

Every wallet, program, and data account has a Pubkey. It is always 32 bytes, and it implements Copy.

Pubkey is a newtype wrapper around [u8; 32]. Under the hood it is simply 32 bytes — the output of the Ed25519 elliptic curve algorithm that Solana uses for all signing. Import it from solana_program::pubkey::Pubkey (or from anchor_lang::prelude::* if you are using Anchor).

Because Pubkey implements Copy, you can assign it to variables and pass it to functions without calling .clone(). This is why you see ctx.accounts.user.key() passed directly into struct fields without any ownership ceremony — the value is copied.

Program Derived Addresses (PDAs) use Pubkey::find_program_address to derive a deterministic address from seeds and your program ID. PDAs are intentionally off the Ed25519 curve, which means no private key can sign for them — only your program can. This is the mechanism behind every vault, escrow, and treasury on Solana.

rust
// Working with Pubkey
use solana_program::pubkey::Pubkey;
use std::str::FromStr;

// Hardcode a known address (e.g., USDC mint)
let usdc_mint = Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
    .expect("valid base58");

// The zero address — all 1s (System Program)
let system_program = Pubkey::default();

// Derive a PDA using seeds
let seeds = &[b"vault", authority.as_ref()];
let (vault_pda, bump) = Pubkey::find_program_address(seeds, &program_id);

If you are still building intuition around ownership and why Copy types behave differently from String, the Rust Ownership for Solana Developers guide covers the ownership model with Solana examples throughout.

Section 07 · Choosing

Picking the right type: a practical guide

A handful of rules cover 95 percent of field choices in real Solana programs.

SOL or token amounts → u64

The lamport system was designed for u64. SPL tokens also use u64. Never use any other numeric type for monetary values. The checked_sub and checked_add methods are mandatory when doing balance arithmetic.

Timestamps → i64

The Solana Clock sysvar exposes unix_timestamp as i64. Match that type in your account struct so you never need a cast. i64 holds any Unix timestamp you will encounter in practice.

Version numbers, bump seeds, decimals → u8

These values stay small and fixed. u8 costs 1 byte, fits in a register, and makes the maximum value explicit in the type. A u8 bump seed cannot accidentally grow to a value outside 0 to 255.

Boolean flags → bool

One byte, two states. Use bool for is_initialized, is_paused, is_frozen, and any other field that is strictly binary. Resist using u8 as a bool — it works but signals to the reader that more states might exist.

Addresses → Pubkey

Never store an address as a String or a [u8; 32] array directly. Pubkey carries semantic meaning: it is an address, it implements Display for base58 output, and it interoperates cleanly with every Solana SDK function.

User text → String with max_len

Pick a conservative maximum length. Every byte costs rent. A title capped at 100 bytes keeps the account small. A description capped at 2000 bytes is large — make sure the use case justifies it. Always annotate with #[max_len(N)] when using Anchor.

For the full picture of how these types fit inside an account struct, including how Anchor derives the total space, read the What Is Anchor? guide, which covers account initialisation and the InitSpace derive macro in detail.

If you are building a production Solana program and want an experienced team to review your account model and type choices, the blockchain development service covers program design, auditing, and mainnet deployment.

Section 08 · FAQ

Common questions about Rust data types in Solana

Direct answers to the questions developers ask when choosing types for their first Solana account structs.

What integer type should I use for lamport balances in Solana?

Use u64 for all lamport and token balances. Solana represents all SOL amounts in lamports, where one SOL equals one billion lamports. The u64 type stores values from zero to about 18.4 quintillion, which is more than enough for any real world balance. u64 is also the type used by AccountInfo.lamports() and by every standard SPL token amount field, so matching that type avoids conversion overhead and keeps your program consistent with the rest of the ecosystem.

Can I use floating point numbers in Solana programs?

You should not use f32 or f64 in on chain Solana programs. Floating point arithmetic produces results that can differ by a single bit depending on the CPU architecture and runtime environment. Because different validator nodes run on different hardware, a computation that uses floats could produce different results on different validators, causing the network to reject your transaction as invalid. The standard fix is to use integer math with an explicit decimal scale — for example, representing a price of $10.25 as the integer 10250 with an implied two decimal precision.

What is the difference between String and &str in Rust?

String is a heap allocated, owned, growable sequence of UTF-8 bytes. &str is a borrowed reference to a string slice that can point into a String or a string literal in program memory. In a Solana program, you generally use String for fields in account structs that will be serialised to the blockchain, because the Borsh serialiser needs an owned, known size value. You use &str for function parameters where you want to borrow without taking ownership. On chain, both end up serialised as a four byte length prefix followed by the UTF-8 bytes.

How many bytes does a Pubkey take in a Solana account?

A Pubkey is always exactly 32 bytes, regardless of what the key represents. Under the hood, Pubkey is a newtype wrapper around [u8; 32]. When Borsh serialises a struct containing a Pubkey field, it writes exactly 32 bytes for it with no length prefix. This fixed size makes Pubkeys efficient to store and easy to reason about when calculating account space. Solana uses 32 byte public keys because they are the output of the Ed25519 elliptic curve algorithm used for all signing.

What is checked arithmetic in Rust and when should I use it?

Checked arithmetic is a set of methods (checked_add, checked_sub, checked_mul, checked_div) on integer types that return Option instead of panicking or wrapping on overflow. In debug builds, Rust panics on integer overflow by default. In release builds, it wraps silently. A wrapped balance looks like a valid u64 but has the completely wrong value. Calling checked_sub(fee) and handling the None case explicitly is the correct way to deduct fees or transfer tokens, because it guarantees your program returns a proper error instead of silently corrupting state.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View blockchain development serviceSee ChainTrust case study

Related service

Blockchain Development

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →