Decimals & Amounts
SPL tokens store raw integer amounts on-chain. Decimals on the mint define how to display human-readable values. Off-by-one decimal errors are among the most expensive bugs in token integrations.
Search across all documentation pages
SPL tokens store raw integer amounts on-chain. Decimals on the mint define how to display human-readable values. Off-by-one decimal errors are among the most expensive bugs in token integrations.
const UI = 1.5;
const raw = BigInt(Math.round(UI * 10 ** decimals));
const display = Number(raw) / 10 ** decimals;pub fn to_raw(ui: f64, decimals: u8) -> Result<u64> {
let factor = 10u64.pow(decimals as u32);
let raw = (ui * factor as f64).round() as u64;
Ok(raw)
}When to reach for this:
transfer_checked decimals on-chainimport { createSolanaRpc, address } from "@solana/kit";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const mint = address("MINT_PUBKEY");
const { value } = await rpc.getAccountInfo(mint, { encoding: "jsonParsed" }).send();
const decimals = value?.data.parsed.info.decimals as number;
function toRawAmount(uiAmount: string, decimals: number): bigint {
const [whole, frac = ""] = uiAmount.split(".");
const padded = (frac + "0".repeat(decimals)).slice(0, decimals);
return BigInt(whole + padded);
}
function toUiAmount(raw: bigint, decimals: number): string {
const s = raw.toString().padStart(decimals + 1, "0");
const whole = s.slice(0, -decimals) || "0";
const frac = s.slice(-decimals).replace(/0+$/, "");
return frac ? `${whole}.${frac}` : whole;
}
const raw = toRawAmount("2.5", decimals);
console.log("raw:", raw.toString());
console.log("ui:", toUiAmount(raw, decimals));use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, TransferChecked, Mint};
#[derive(Accounts)]
pub struct SafeTransfer<'info> {
pub mint: Account<'info, Mint>,
#[account(mut)]
pub from: Account<'info, TokenAccount>,
#[account(mut)]
pub to: Account<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
pub fn safe_transfer(ctx: Context<SafeTransfer>, amount: u64) -> Result<()> {
let decimals = ctx.accounts.mint.decimals;
let cpi = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
);
token::transfer_checked(cpi, amount, decimals)?;
Ok(())
}What this demonstrates:
jsonParsed exposes tokenAmount.amount, decimals, and uiAmounttransfer_checked binds amount to expected decimals in programsdecimals: u8 is immutable after creation (0-9 typical; up to 255 allowed)amount: u64 is always raw base unitstransfer_checked fails if instruction decimals != mint decimals| Asset style | Decimals | 1.0 token raw |
|---|---|---|
| USDC-like | 6 | 1_000_000 |
| SOL-wrapped | 9 | 1_000_000_000 |
| Whole-number NFT | 0 | 1 |
use anchor_lang::solana_program::program_error::ProgramError;
pub fn mul_scaled(a: u64, b: u64) -> Result<u64, ProgramError> {
a.checked_mul(b).ok_or(ProgramError::InvalidArgument)
}1.1 * 1e6 rounding errors. Fix: string/BigInt conversion like toRawAmount.transfer_checked with mint decimals.spl-token CLI - CLI amount arguments are UI amounts, so a "raw" value overspends by 10^decimals. Fix: pass 2.5, not 2500000; reserve raw units for instruction data.| Alternative | Use When | Don't Use When |
|---|---|---|
uiAmount from RPC | Quick display | Building instructions (use amount string) |
| Fixed-decimal constants | Single-asset app | Multi-token portfolio |
Decimal libraries (decimal.js) | Financial precision | Simple wallet with BigInt sufficient |
On the mint account only. Token accounts store raw amount.
No. Immutable at mint initialization.
Integer stored on-chain - the only value instructions use.
Industry convention matching issuer fiat precision.
Transfer that includes expected decimals - prevents certain cross-asset mistakes in programs.
Yes. Common for NFTs (supply 1, amount 1).
The opposite of the instruction encoding: spl-token amount arguments are UI amounts, and the CLI scales them by the mint's decimals for you. spl-token transfer <MINT> 2.5 <RECIPIENT> sends 2.5 tokens; passing 2500000 would send 2,500,000 tokens. Raw base units appear only in instruction data (and in spl-token display output).
Same mint-level decimals field and raw amount semantics.
~1.8e19 raw units - combine with decimals for realistic UI cap planning.
Product decision - always document. Payments often floor; displays may round.
Often raw or fixed-point with explicit decimals in account layout - read protocol docs.
Use BigInt for raw amounts in kit 7.0.0 transaction building when values exceed Number.MAX_SAFE_INTEGER.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026