Solana Program Development: From Zero to Hero
Solana programs (smart contracts) are the backbone of the Solana ecosystem. Unlike Ethereum's EVM, Solana uses a unique architecture that enables high throughput and low costs. This guide will take you from beginner to advanced Solana program developer.
Understanding Solana Architecture
Key Differences from Ethereum
- Accounts vs Storage: Solana uses accounts instead of contract storage
- Program vs Contract: Programs are stateless, accounts hold state
- Rent: Accounts must maintain minimum balance for rent exemption
- Compute Units: Programs have compute unit limits per transaction
Solana Program Types
- System Programs: Core blockchain functionality
- Token Programs: SPL token operations
- Custom Programs: Your application logic
Setting Up Development Environment
1. Install Required Tools
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install Solana CLI
sh -c "$(curl -sSfL https://release.solana.com/v1.17.0/install)"
# Install Anchor
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
avm install latest
avm use latest
2. Initialize Project
anchor init my-solana-program
cd my-solana-program
Building Your First Program
1. Basic Program Structure
use anchor_lang::prelude::*;
declare_id!("11111111111111111111111111111111");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = data;
my_account.authority = ctx.accounts.authority.key();
Ok(())
}
pub fn update_data(ctx: Context<UpdateData>, new_data: u64) -> Result<()> {
let my_account = &mut ctx.accounts.my_account;
my_account.data = new_data;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = authority,
space = 8 + MyAccount::INIT_SPACE
)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct UpdateData<'info> {
#[account(
mut,
constraint = my_account.authority == authority.key()
)]
pub my_account: Account<'info, MyAccount>,
pub authority: Signer<'info>,
}
#[account]
pub struct MyAccount {
pub data: u64,
pub authority: Pubkey,
}
impl MyAccount {
pub const INIT_SPACE: usize = 8 + 8 + 32;
}
2. Advanced Program Features
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
#[program]
pub mod advanced_program {
use super::*;
pub fn create_pool(
ctx: Context<CreatePool>,
pool_id: u64,
token_a_amount: u64,
token_b_amount: u64,
) -> Result<()> {
let pool = &mut ctx.accounts.pool;
pool.id = pool_id;
pool.token_a_mint = ctx.accounts.token_a_mint.key();
pool.token_b_mint = ctx.accounts.token_b_mint.key();
pool.token_a_amount = token_a_amount;
pool.token_b_amount = token_b_amount;
pool.authority = ctx.accounts.authority.key();
pool.bump = ctx.bumps.pool;
// Transfer tokens to pool
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.authority_token_a.to_account_info(),
to: ctx.accounts.pool_token_a.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
);
token::transfer(transfer_ctx, token_a_amount)?;
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.authority_token_b.to_account_info(),
to: ctx.accounts.pool_token_b.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
);
token::transfer(transfer_ctx, token_b_amount)?;
Ok(())
}
pub fn swap(
ctx: Context<Swap>,
amount_in: u64,
minimum_amount_out: u64,
) -> Result<()> {
let pool = &ctx.accounts.pool;
// Calculate swap amount using constant product formula
let amount_out = calculate_swap_amount(
pool.token_a_amount,
pool.token_b_amount,
amount_in,
)?;
require!(
amount_out >= minimum_amount_out,
ErrorCode::InsufficientOutputAmount
);
// Update pool reserves
let pool_account = &mut ctx.accounts.pool;
pool_account.token_a_amount += amount_in;
pool_account.token_b_amount -= amount_out;
// Transfer tokens
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.user_token_a.to_account_info(),
to: ctx.accounts.pool_token_a.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
);
token::transfer(transfer_ctx, amount_in)?;
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.pool_token_b.to_account_info(),
to: ctx.accounts.user_token_b.to_account_info(),
authority: ctx.accounts.pool_authority.to_account_info(),
},
);
token::transfer(transfer_ctx, amount_out)?;
emit!(SwapEvent {
user: ctx.accounts.user.key(),
amount_in,
amount_out,
pool_id: pool.id,
});
Ok(())
}
}
#[derive(Accounts)]
#[instruction(pool_id: u64)]
pub struct CreatePool<'info> {
#[account(
init,
payer = authority,
space = 8 + Pool::INIT_SPACE,
seeds = [b"pool", pool_id.to_le_bytes().as_ref()],
bump
)]
pub pool: Account<'info, Pool>,
pub token_a_mint: Account<'info, token::Mint>,
pub token_b_mint: Account<'info, token::Mint>,
#[account(
init,
payer = authority,
token::mint = token_a_mint,
token::authority = pool_authority,
)]
pub pool_token_a: Account<'info, TokenAccount>,
#[account(
init,
payer = authority,
token::mint = token_b_mint,
token::authority = pool_authority,
)]
pub pool_token_b: Account<'info, TokenAccount>,
#[account(
init,
payer = authority,
seeds = [b"pool_authority", pool.key().as_ref()],
bump
)]
pub pool_authority: SystemAccount<'info>,
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut)]
pub authority_token_a: Account<'info, TokenAccount>,
#[account(mut)]
pub authority_token_b: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
#[derive(Accounts)]
pub struct Swap<'info> {
#[account(mut)]
pub pool: Account<'info, Pool>,
#[account(mut)]
pub pool_token_a: Account<'info, TokenAccount>,
#[account(mut)]
pub pool_token_b: Account<'info, TokenAccount>,
#[account(mut)]
pub user: Signer<'info>,
#[account(mut)]
pub user_token_a: Account<'info, TokenAccount>,
#[account(mut)]
pub user_token_b: Account<'info, TokenAccount>,
/// CHECK: This is the pool authority PDA
pub pool_authority: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}
#[account]
pub struct Pool {
pub id: u64,
pub token_a_mint: Pubkey,
pub token_b_mint: Pubkey,
pub token_a_amount: u64,
pub token_b_amount: u64,
pub authority: Pubkey,
pub bump: u8,
}
impl Pool {
pub const INIT_SPACE: usize = 8 + 8 + 32 + 32 + 8 + 8 + 32 + 1;
}
#[event]
pub struct SwapEvent {
pub user: Pubkey,
pub amount_in: u64,
pub amount_out: u64,
pub pool_id: u64,
}
#[error_code]
pub enum ErrorCode {
#[msg("Insufficient output amount")]
InsufficientOutputAmount,
#[msg("Invalid swap amount")]
InvalidSwapAmount,
}
fn calculate_swap_amount(
reserve_in: u64,
reserve_out: u64,
amount_in: u64,
) -> Result<u64> {
if reserve_in == 0 || reserve_out == 0 {
return Err(ErrorCode::InvalidSwapAmount.into());
}
let amount_in_with_fee = amount_in * 997;
let numerator = amount_in_with_fee * reserve_out;
let denominator = reserve_in * 1000 + amount_in_with_fee;
Ok(numerator / denominator)
}
Testing Your Programs
1. Unit Testing
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { expect } from "chai";
import { MySolanaProgram } from "../target/types/my_solana_program";
describe("my-solana-program", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.MySolanaProgram as Program<MySolanaProgram>;
const wallet = provider.wallet;
it("Initializes account", async () => {
const [myAccount] = await PublicKey.findProgramAddress(
[Buffer.from("my_account"), wallet.publicKey.toBuffer()],
program.programId
);
await program.methods
.initialize(new anchor.BN(42))
.accounts({
myAccount,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
const account = await program.account.myAccount.fetch(myAccount);
expect(account.data.toNumber()).to.equal(42);
expect(account.authority.toString()).to.equal(wallet.publicKey.toString());
});
it("Updates account data", async () => {
const [myAccount] = await PublicKey.findProgramAddress(
[Buffer.from("my_account"), wallet.publicKey.toBuffer()],
program.programId
);
await program.methods
.updateData(new anchor.BN(100))
.accounts({
myAccount,
authority: wallet.publicKey,
})
.rpc();
const account = await program.account.myAccount.fetch(myAccount);
expect(account.data.toNumber()).to.equal(100);
});
});
2. Integration Testing
describe("Pool Integration Tests", () => {
it("Creates pool and performs swap", async () => {
// Create token mints
const tokenAMint = await createMint(provider.connection, wallet.payer);
const tokenBMint = await createMint(provider.connection, wallet.payer);
// Create user token accounts
const userTokenA = await createAccount(
provider.connection,
wallet.payer,
tokenAMint,
wallet.publicKey
);
const userTokenB = await createAccount(
provider.connection,
wallet.payer,
tokenBMint,
wallet.publicKey
);
// Mint tokens to user
await mintTo(
provider.connection,
wallet.payer,
tokenAMint,
userTokenA,
wallet.publicKey,
1000000
);
// Create pool
const poolId = new anchor.BN(Date.now());
await program.methods
.createPool(poolId, new anchor.BN(100000), new anchor.BN(200000))
.accounts({
pool: poolPDA,
tokenAMint,
tokenBMint,
poolTokenA: poolTokenAPDA,
poolTokenB: poolTokenBPDA,
poolAuthority: poolAuthorityPDA,
authority: wallet.publicKey,
authorityTokenA: userTokenA,
authorityTokenB: userTokenB,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: SYSVAR_RENT_PUBKEY,
})
.rpc();
// Perform swap
await program.methods
.swap(new anchor.BN(1000), new anchor.BN(1900))
.accounts({
pool: poolPDA,
poolTokenA: poolTokenAPDA,
poolTokenB: poolTokenBPDA,
user: wallet.publicKey,
userTokenA,
userTokenB,
poolAuthority: poolAuthorityPDA,
tokenProgram: TOKEN_PROGRAM_ID,
})
.rpc();
// Verify swap results
const poolAccount = await program.account.pool.fetch(poolPDA);
expect(poolAccount.tokenAAmount.toNumber()).to.be.greaterThan(100000);
expect(poolAccount.tokenBAmount.toNumber()).to.be.lessThan(200000);
});
});
Deployment and Mainnet Considerations
1. Program Deployment
# Build program
anchor build
# Deploy to devnet
anchor deploy --provider.cluster devnet
# Deploy to mainnet
anchor deploy --provider.cluster mainnet
2. Security Best Practices
// Always validate inputs
pub fn safe_transfer(
ctx: Context<SafeTransfer>,
amount: u64,
) -> Result<()> {
require!(amount > 0, ErrorCode::InvalidAmount);
require!(amount <= MAX_TRANSFER_AMOUNT, ErrorCode::AmountTooLarge);
// Additional validation logic
Ok(())
}
// Use proper access controls
#[derive(Accounts)]
pub struct AdminOnly<'info> {
#[account(
constraint = admin.key() == ADMIN_PUBKEY
)]
pub admin: Signer<'info>,
}
// Implement proper error handling
#[error_code]
pub enum ErrorCode {
#[msg("Invalid amount")]
InvalidAmount,
#[msg("Amount too large")]
AmountTooLarge,
#[msg("Unauthorized access")]
Unauthorized,
}
3. Gas Optimization
// Use efficient data structures
#[account]
pub struct OptimizedAccount {
pub data: u64, // 8 bytes
pub flags: u8, // 1 byte
pub bump: u8, // 1 byte
// Total: 10 bytes (padded to 16)
}
// Avoid unnecessary computations
pub fn efficient_calculation(
ctx: Context<EfficientCalculation>,
input: u64,
) -> Result<()> {
// Cache frequently used values
let account = &ctx.accounts.account;
let cached_value = account.data;
// Use bitwise operations when possible
let result = cached_value << 1; // Multiply by 2
Ok(())
}
Advanced Patterns
1. Program Derived Addresses (PDAs)
#[derive(Accounts)]
#[instruction(seed: u64)]
pub struct CreateAccount<'info> {
#[account(
init,
payer = user,
space = 8 + MyAccount::INIT_SPACE,
seeds = [b"my_account", user.key().as_ref(), seed.to_le_bytes().as_ref()],
bump
)]
pub my_account: Account<'info, MyAccount>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
2. Cross-Program Invocations (CPIs)
use anchor_spl::token::{self, Token, TokenAccount, Transfer};
pub fn transfer_tokens(
ctx: Context<TransferTokens>,
amount: u64,
) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
3. Event Emission
#[event]
pub struct TransferEvent {
pub from: Pubkey,
pub to: Pubkey,
pub amount: u64,
pub timestamp: i64,
}
pub fn emit_transfer_event(
from: Pubkey,
to: Pubkey,
amount: u64,
) -> Result<()> {
emit!(TransferEvent {
from,
to,
amount,
timestamp: Clock::get()?.unix_timestamp,
});
Ok(())
}
Conclusion
Solana program development offers unique advantages with its high-performance architecture. By following these patterns and best practices, you can build efficient, secure, and scalable applications on Solana.
Key takeaways:
- Understand Solana's account-based model
- Use Anchor framework for faster development
- Implement proper security measures
- Test thoroughly before deployment
- Optimize for gas efficiency
- Use PDAs for deterministic account generation
For more advanced topics, explore my guides on Solana program optimization and DeFi protocol development.