Solana Program Development: From Zero to Hero

January 20, 2024 (1y ago)

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

Solana Program Types

  1. System Programs: Core blockchain functionality
  2. Token Programs: SPL token operations
  3. 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:

For more advanced topics, explore my guides on Solana program optimization and DeFi protocol development.

Resources