Appearance
Working with SPL Tokens
Tokens on the SVM are managed by on-chain programs — not by the runtime itself. The SPL Token Program is the standard implementation for fungible and non-fungible tokens. Its successor, Token-2022 (Token Extensions), adds advanced features while maintaining the same core account model. Both are available on Zink.
Upstream-compatible
The Token Program, Token-2022, and Associated Token Account program on Zink are identical to Solana mainnet. Existing token operations, client code, and CPIs work without modification.
Core concepts
The token model revolves around three account types:
| Account type | Program | Purpose |
|---|---|---|
| Mint account | Token Program or Token-2022 | Defines a token: supply, decimals, authorities. One per token. |
| Token account | Token Program or Token-2022 | Holds a balance of a specific mint for a specific owner. Many per token. |
| Associated Token Account (ATA) | ATA Program | A deterministically-derived token account for a given wallet + mint pair. |
┌──────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ Mint Account │ │ Token Account │ │ Token Account │
│ │ │ │ │ │
│ supply: 1M │◄──────│ mint: <this mint> │ │ mint: <this mint> │
│ decimals: 6 │ │ owner: Alice │ │ owner: Bob │
│ mint_auth: X │ │ amount: 400,000 │ │ amount: 600,000 │
│ freeze_auth: Y│ │ │ │ │
└──────────────┘ └─────────────────────┘ └──────────────────────┘Mint accounts
A mint account defines a token type. Key fields:
| Field | Description |
|---|---|
supply | Total number of tokens currently in circulation |
decimals | Number of decimal places (e.g., 6 means 1 token = 1,000,000 base units) |
mint_authority | The public key (or PDA) authorized to mint new tokens. Can be set to None to cap supply permanently. |
freeze_authority | The public key authorized to freeze/thaw token accounts. Can be None. |
is_initialized | Whether the mint has been initialized |
Creating a mint
bash
# CLI
spl-token create-token --decimals 6rust
// On-chain via CPI
invoke(
&spl_token::instruction::initialize_mint(
token_program.key,
mint.key,
mint_authority.key,
Some(freeze_authority.key),
decimals,
)?,
&[mint.clone(), rent_sysvar.clone()],
)?;Token accounts
A token account holds a balance of a specific token (mint) for a specific owner. Key fields:
| Field | Description |
|---|---|
mint | The mint this token account is associated with |
owner | The wallet that controls this token account (can transfer, burn, close) |
amount | Current token balance in base units |
delegate | Optional delegated authority for a one-time transfer/burn up to delegated_amount |
state | Initialized, Frozen, or Uninitialized |
close_authority | Optional authority that can close this account (defaults to owner) |
The owner of a token account (the wallet) is distinct from the account's on-chain owner field (which is the Token Program). The Token Program uses the owner field in the token account data to authorize operations.
Associated Token Accounts (ATAs)
Manually creating token accounts is cumbersome — you need to generate a keypair, allocate space, and initialize. The Associated Token Account (ATA) program solves this by deterministically deriving a token account address for any wallet + mint pair:
ATA address = PDA(wallet_pubkey, token_program_id, mint_pubkey)This means every wallet has exactly one canonical ATA per token. If it doesn't exist yet, anyone can create it (the ATA program's create instruction is permissionless — the payer funds the rent-exempt minimum).
typescript
// TypeScript
import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from "@solana/spl-token";
const ata = await getAssociatedTokenAddress(mint, wallet);
// If ata doesn't exist yet:
transaction.add(createAssociatedTokenAccountInstruction(payer, ata, wallet, mint));Zink recommendation
Always use ATAs when sending tokens to user wallets. This ensures consistent token account addresses across ecosystem programs and frontends.
Token operations
Transfer
rust
// CPI from your program
invoke(
&spl_token::instruction::transfer(
token_program.key,
source.key,
destination.key,
authority.key,
&[],
amount,
)?,
&[source.clone(), destination.clone(), authority.clone()],
)?;Mint
Requires the mint_authority to sign:
rust
invoke_signed(
&spl_token::instruction::mint_to(
token_program.key,
mint.key,
destination.key,
mint_authority_pda.key,
&[],
amount,
)?,
&[mint.clone(), destination.clone(), mint_authority_pda.clone()],
&[&[b"mint_authority", &[bump]]],
)?;Burn
Destroys tokens, reducing the mint's supply:
rust
invoke(
&spl_token::instruction::burn(
token_program.key,
token_account.key,
mint.key,
authority.key,
&[],
amount,
)?,
&[token_account.clone(), mint.clone(), authority.clone()],
)?;Close account
Closes a token account and reclaims the rent-exempt lamports. The token account must have a zero balance:
rust
invoke(
&spl_token::instruction::close_account(
token_program.key,
token_account.key,
destination.key, // receives the lamports
authority.key,
&[],
)?,
&[token_account.clone(), destination.clone(), authority.clone()],
)?;Authorities
Tokens have two key authorities:
| Authority | Controls | Set on |
|---|---|---|
| Mint authority | Can mint new tokens. Set to None to make supply fixed. | Mint account |
| Freeze authority | Can freeze/thaw individual token accounts, preventing transfers. | Mint account |
Both can be transferred to a new key or revoked (set_authority instruction). For program-controlled tokens, these authorities are typically PDAs so the program can sign mint/freeze operations via CPI.
bash
# Transfer mint authority to a PDA or multisig
spl-token authorize <MINT> mint <NEW_AUTHORITY>
# Revoke mint authority permanently
spl-token authorize <MINT> mint --disableToken-2022 (Token Extensions)
Token-2022 is the next-generation token program. It is a separate program (TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb) that supports all original Token Program functionality plus extensions — optional features that are enabled per-mint or per-token-account at creation time:
| Extension | Description |
|---|---|
| Transfer fees | Automatic fee on every transfer, collected in a withheld balance |
| Interest-bearing | UI-layer interest rate for display purposes (does not actually accrue tokens) |
| Non-transferable | Soulbound tokens that cannot be transferred after minting |
| Permanent delegate | A delegate that can never be revoked — can always transfer or burn |
| Transfer hook | Calls a custom program on every transfer for validation or side effects |
| Metadata | On-chain token metadata stored directly in the mint account |
| Confidential transfers | Encrypted balances and transfer amounts using zero-knowledge proofs |
| Default account state | New token accounts start as frozen (require explicit thaw) |
| Group / member | Token grouping for collections |
When to use Token-2022
- Use the original Token Program for maximum compatibility — it is supported by all wallets, DEXes, and tools.
- Use Token-2022 when you need one or more of its extensions. Note that not all ecosystem tools support Token-2022 tokens yet, so verify compatibility with wallets and frontends in advance.
Interacting with Token-2022 via CPI
The instruction interface is the same as the original Token Program — you just target a different program ID:
rust
// Use the Token-2022 program ID instead
let token_program_id = spl_token_2022::ID;
invoke(
&spl_token_2022::instruction::transfer_checked(
&token_program_id,
source.key,
mint.key,
destination.key,
authority.key,
&[],
amount,
decimals,
)?,
&[source.clone(), mint.clone(), destination.clone(), authority.clone()],
)?;Note: Token-2022 prefers transfer_checked (which validates decimals) over plain transfer.
Token account sizes
| Account type | Size (bytes) | Notes |
|---|---|---|
| Mint (Token Program) | 82 | Fixed |
| Token account (Token Program) | 165 | Fixed |
| Mint (Token-2022) | 82 + extensions | Variable depending on enabled extensions |
| Token account (Token-2022) | 165 + extensions | Variable depending on enabled extensions |
Query the exact size and rent-exempt minimum before creating accounts. See Accounts — Rent and Fees.