SPL Token Development (Solana)
EVM developers coming to Solana usually expect an ERC-20 analogue with similar concepts. Reality is very different. In Solana there's no concept of "token contract" — there's Token Program (system program) managing all tokens, and Mint account — account with specific token metadata. Logic for customization is implemented not through contract inheritance, but through combination of Token-2022 extensions and separate programs (like Transfer Hook).
Token Program vs. Token-2022
On Solana exist two standards:
spl-token (Token Program) — original 2020 standard. Simple, battle-tested, supported by all wallets and DEX. Features: mint, burn, transfer, freeze, multisig authority.
Token-2022 (Token Extensions Program) — new standard with extensible architecture. Recommended for new projects. Key extensions:
| Extension | Purpose |
|---|---|
TransferFee |
Fee on each transfer (like reflection/tax token) |
TransferHook |
Call custom program on each transfer |
ConfidentialTransfer |
Private balances via ZK proofs (ElGamal encryption) |
PermanentDelegate |
Address with permanent right to burn/transfer tokens |
MintCloseAuthority |
Ability to close Mint account (return rent) |
NonTransferable |
Soulbound tokens — can't transfer |
InterestBearingMint |
On-chain interest accrual |
MetadataPointer + TokenMetadata |
Metadata right in Mint account |
Creating Mint account
Using @solana/spl-token (TypeScript SDK):
import {
createMint,
createAssociatedTokenAccount,
mintTo,
TOKEN_2022_PROGRAM_ID,
ExtensionType,
getMintLen,
createInitializeMintInstruction,
createInitializeTransferFeeConfigInstruction,
} from "@solana/spl-token";
import { Connection, Keypair, SystemProgram, Transaction } from "@solana/web3.js";
async function createTokenWithTransferFee(
connection: Connection,
payer: Keypair,
mintAuthority: PublicKey,
decimals: number,
feeBasisPoints: number, // 100 = 1%
maxFee: bigint, // maximum fee in lamports
) {
const mintKeypair = Keypair.generate();
// calculate account size with needed extensions
const extensions = [ExtensionType.TransferFeeConfig];
const mintLen = getMintLen(extensions);
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
const transaction = new Transaction().add(
// create account of needed size
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: mintLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
}),
// initialize TransferFee extension BEFORE mint init
createInitializeTransferFeeConfigInstruction(
mintKeypair.publicKey,
payer.publicKey, // transferFeeConfigAuthority
payer.publicKey, // withdrawWithheldAuthority
feeBasisPoints,
maxFee,
TOKEN_2022_PROGRAM_ID,
),
// initialize mint
createInitializeMintInstruction(
mintKeypair.publicKey,
decimals,
mintAuthority,
null, // freeze authority — null if not needed
TOKEN_2022_PROGRAM_ID,
),
);
await sendAndConfirmTransaction(connection, transaction, [payer, mintKeypair]);
return mintKeypair.publicKey;
}
Critical moment: extensions are initialized before InitializeMint. Counterintuitive for EVM developers, but that's Token-2022 architecture — configure extensions first, then finalize mint.
Associated Token Accounts
In Solana each user must have separate Token Account for each token. Associated Token Account (ATA) — deterministic address derived from user wallet and mint address. ATA is created once, address always predictable:
import { getAssociatedTokenAddress, createAssociatedTokenAccountInstruction } from "@solana/spl-token";
const ata = await getAssociatedTokenAddress(
mintAddress,
walletAddress,
false, // allowOwnerOffCurve — usually false
TOKEN_2022_PROGRAM_ID,
);
// create ATA if it doesn't exist (idempotent)
const createATAIx = createAssociatedTokenAccountInstruction(
payer.publicKey, // who pays rent
ata,
walletAddress, // owner
mintAddress,
TOKEN_2022_PROGRAM_ID,
);
Rent for Token Account (~0.002 SOL) is paid by creator — important UX moment. Many projects assume creating ATA for users on first operation (airdrop, purchase), including rent cost in operational expenses.
Transfer Hook: custom logic on transfers
Transfer Hook — most powerful extension, allows executing arbitrary logic on each transfer. This is Solana analogue of ERC-20 hooks or transfer callbacks.
Custom program (Transfer Hook program) is called automatically by Token-2022 on each transfer. In program you can implement: whitelist/blacklist check, royalty for tokens, transfer blocking in locked period, protocol state integration.
// Transfer Hook program (Anchor framework)
use anchor_lang::prelude::*;
use spl_transfer_hook_interface::instruction::ExecuteInstruction;
#[program]
pub mod transfer_hook {
use super::*;
// this function is called by Token-2022 on each transfer
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
let sender = &ctx.accounts.source_token;
let receiver = &ctx.accounts.destination_token;
// check whitelist
let config = &ctx.accounts.hook_config;
require!(
config.whitelist.contains(&receiver.owner),
TransferError::ReceiverNotWhitelisted
);
// log transfer for off-chain analysis
emit!(TransferEvent {
from: sender.owner,
to: receiver.owner,
amount,
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
}
Transfer Hook requires all additional accounts needed by hook to be passed in transaction beforehand. This complicates client code — you need to know which accounts the hook needs before sending transaction. SDK provides addExtraAccountMetasForExecute for automatic resolution.
Token metadata
Metadata (name, symbol, image, description) is stored two ways:
Metaplex Metadata Program — legacy approach, standard for NFTs and most fungible tokens today. Separate account with metadata, linked to mint. Wide wallet and marketplace support.
Token Metadata Extension (Token-2022) — new approach, metadata right in Mint account. Simpler architecturally, doesn't require Metaplex. Wallet support growing, but still less universal than Metaplex.
For production fungible token (DeFi, governance) in 2024–2025 Metaplex is recommended — maximum ecosystem compatibility (wallets, DEX, price aggregators).
Working with Anchor framework
Most Solana programs are written in Rust with Anchor framework. Anchor generates IDL (Interface Definition Language) — EVM ABI analogue — simplifying client integration:
anchor build # compilation + IDL generation
anchor test # tests on localnet
anchor deploy # deploy on devnet/mainnet
For token without custom program Anchor isn't needed — @solana/spl-token SDK is enough. Anchor is necessary if building dApp on top of token with custom business logic (staking, vesting, governance).







