Skip to content

Latest commit

 

History

History
759 lines (618 loc) · 15.1 KB

File metadata and controls

759 lines (618 loc) · 15.1 KB

Best Practices for Gleam Solana Development

A comprehensive guide to writing secure, efficient, and maintainable Solana programs in Gleam.

Table of Contents

  1. Security Best Practices
  2. Performance Optimization
  3. Code Quality
  4. Testing Strategies
  5. Deployment Guidelines
  6. Common Pitfalls
  7. Design Patterns

Security Best Practices

1. Validate All Inputs

❌ Bad:

pub fn transfer(amount: Int) -> Expression {
  Return(IntLiteral(amount))  // No validation!
}

Good:

pub fn transfer(amount: Int) -> Result(Expression, String) {
  case amount > 0 {
    True -> Ok(Return(IntLiteral(amount)))
    False -> Error("Amount must be positive")
  }
}

2. Check for Integer Overflow

❌ Bad:

pub fn add_balance(current: Int, deposit: Int) -> Expression {
  Return(Add(IntLiteral(current), IntLiteral(deposit)))
  // Could overflow!
}

Good:

pub fn add_balance(current: Int, deposit: Int) -> Result(Expression, String) {
  case current + deposit {
    sum if sum >= current && sum >= deposit -> 
      Ok(Return(Add(IntLiteral(current), IntLiteral(deposit))))
    _ -> Error("Integer overflow detected")
  }
}

3. Prevent Reentrancy

When designing programs that call other programs:

pub type ProgramState {
  ProgramState(
    locked: Bool,
    balance: Int,
  )
}

pub fn withdraw(state: ProgramState, amount: Int) -> Result(ProgramState, String) {
  // Check lock first
  case state.locked {
    True -> Error("Reentrant call detected")
    False -> {
      // Set lock
      let locked_state = ProgramState(..state, locked: True)
      
      // Perform operation
      case state.balance >= amount {
        True -> {
          let new_balance = state.balance - amount
          Ok(ProgramState(locked: False, balance: new_balance))
        }
        False -> Error("Insufficient balance")
      }
    }
  }
}

4. Validate Account Ownership

pub type AccountInfo {
  AccountInfo(
    key: String,
    owner: String,
    lamports: Int,
    is_signer: Bool,
    is_writable: Bool,
  )
}

pub fn validate_owner(
  account: AccountInfo,
  expected_owner: String
) -> Result(Nil, String) {
  case account.owner == expected_owner {
    True -> Ok(Nil)
    False -> Error("Invalid account owner")
  }
}

5. Check Signer Authority

pub fn validate_signer(account: AccountInfo) -> Result(Nil, String) {
  case account.is_signer {
    True -> Ok(Nil)
    False -> Error("Account must be a signer")
  }
}

pub fn validate_writable(account: AccountInfo) -> Result(Nil, String) {
  case account.is_writable {
    True -> Ok(Nil)
    False -> Error("Account must be writable")
  }
}

6. Avoid Division by Zero

❌ Bad:

pub fn calculate_share(total: Int, participants: Int) -> Expression {
  Return(
    compiler.Divide(
      IntLiteral(total),
      IntLiteral(participants)
    )
  )
}

Good:

pub fn calculate_share(total: Int, participants: Int) -> Result(Expression, String) {
  case participants {
    0 -> Error("Cannot divide by zero participants")
    n if n > 0 -> Ok(Return(
      compiler.Divide(
        IntLiteral(total),
        IntLiteral(participants)
      )
    ))
    _ -> Error("Participants must be positive")
  }
}

7. Use Explicit Error Messages

❌ Bad:

pub fn process(x: Int) -> Result(Int, String) {
  case x > 0 {
    True -> Ok(x)
    False -> Error("Error")  // Not helpful!
  }
}

Good:

pub fn process(x: Int) -> Result(Int, String) {
  case x > 0 {
    True -> Ok(x)
    False -> Error("Input must be positive. Got: " <> int.to_string(x))
  }
}

Performance Optimization

1. Minimize Instructions

Every instruction costs compute units.

❌ Inefficient:

pub fn calculate() -> Expression {
  Return(
    Add(
      Add(
        Add(IntLiteral(1), IntLiteral(2)),
        IntLiteral(3)
      ),
      IntLiteral(4)
    )
  )
  // Generates many ADD instructions
}

Efficient:

pub fn calculate() -> Expression {
  Return(IntLiteral(10))  // Pre-compute when possible
}

2. Batch Operations

❌ Bad:

pub fn update_three_values(a: Int, b: Int, c: Int) -> List(Expression) {
  [
    Return(IntLiteral(a)),
    Return(IntLiteral(b)),
    Return(IntLiteral(c)),
  ]
  // Three separate programs
}

Good:

pub fn update_three_values(a: Int, b: Int, c: Int) -> Expression {
  // Combine into single result
  Return(Add(Add(IntLiteral(a), IntLiteral(b)), IntLiteral(c)))
}

3. Avoid Redundant Calculations

❌ Bad:

pub fn complex_calc(x: Int) -> Expression {
  let doubled = Multiply(IntLiteral(x), IntLiteral(2))
  Return(
    Add(
      Multiply(IntLiteral(x), IntLiteral(2)),  // Calculated again!
      Multiply(IntLiteral(x), IntLiteral(2))   // And again!
    )
  )
}

Good:

pub fn complex_calc(x: Int) -> Expression {
  // Pre-calculate common values
  let doubled = x * 2
  Return(Add(IntLiteral(doubled), IntLiteral(doubled)))
}

4. Use 64-bit Operations

On Solana, 64-bit and 32-bit operations cost the same.

Good:

// Use Add64Reg instead of Add32Reg
// Same cost, more precision
Instruction(Add64Reg, R0, R1, 0, 0)

5. Minimize Syscalls

Syscalls are expensive (100-1000 CU each).

// Group syscalls together
// Avoid calling same syscall multiple times
// Cache results when possible

Code Quality

1. Use Type Annotations

❌ Bad:

pub fn process(x, y) {
  x + y
}

Good:

pub fn process(x: Int, y: Int) -> Int {
  x + y
}

2. Write Self-Documenting Code

❌ Bad:

pub fn calc(a: Int, b: Int, c: Int) -> Int {
  a * b / c
}

Good:

pub fn calculate_proportional_share(
  total_amount: Int,
  user_share: Int,
  total_shares: Int,
) -> Int {
  total_amount * user_share / total_shares
}

3. Keep Functions Small

❌ Bad:

pub fn process_everything(data: Data) -> Result(Output, String) {
  // 200 lines of code doing many things...
}

Good:

pub fn process_everything(data: Data) -> Result(Output, String) {
  use validated <- result.try(validate_data(data))
  use processed <- result.try(process_data(validated))
  use output <- result.try(format_output(processed))
  Ok(output)
}

fn validate_data(data: Data) -> Result(Data, String) { ... }
fn process_data(data: Data) -> Result(Processed, String) { ... }
fn format_output(processed: Processed) -> Result(Output, String) { ... }

4. Use Meaningful Names

❌ Bad:

pub fn f(x: Int, y: Int) -> Int {
  let z = x * y
  let w = z / 100
  w
}

Good:

pub fn calculate_fee(amount: Int, fee_basis_points: Int) -> Int {
  let fee_amount = amount * fee_basis_points
  let fee_in_tokens = fee_amount / 10000  // basis points to decimal
  fee_in_tokens
}

5. Document Complex Logic

/// Calculate slippage-adjusted output amount for AMM swap
/// 
/// Uses constant product formula: x * y = k
/// Where x = input_reserve, y = output_reserve
/// 
/// Formula: output = (input * output_reserve) / (input_reserve + input)
/// Then apply slippage tolerance
pub fn calculate_swap_output(
  input_amount: Int,
  input_reserve: Int,
  output_reserve: Int,
  slippage_bps: Int,
) -> Result(Int, String) {
  // Implementation...
}

Testing Strategies

1. Test Edge Cases

import gleeunit/should

pub fn test_transfer_edge_cases() {
  // Test zero amount
  transfer(0) |> should.be_error
  
  // Test negative amount
  transfer(-100) |> should.be_error
  
  // Test maximum value
  transfer(9_223_372_036_854_775_807) |> should.be_ok
  
  // Test overflow
  transfer(9_223_372_036_854_775_807)
  |> result.try(fn(_) { transfer(1) })
  |> should.be_error
}

2. Use LiteSVM for Integration Tests

#[test]
fn test_token_transfer() {
    let mut svm = LiteSVM::new();
    let program_data = load_bpf_program("../build/token.so");
    let program_id = Pubkey::new_unique();
    
    svm.add_program(program_id, &program_data);
    
    // Test successful transfer
    let result = execute_transfer(&mut svm, program_id, 100);
    assert!(result.is_ok());
    
    // Test insufficient balance
    let result = execute_transfer(&mut svm, program_id, 10000);
    assert!(result.is_err());
}

3. Test All Code Paths

pub fn test_all_instruction_types() {
  // Test each instruction variant
  process_instruction(Initialize(100)) |> should.be_ok
  process_instruction(Deposit("alice", 50)) |> should.be_ok
  process_instruction(Withdraw("bob", 25)) |> should.be_ok
  process_instruction(Close) |> should.be_ok
}

4. Verify Bytecode Output

pub fn test_bytecode_generation() {
  let program = Return(IntLiteral(42))
  let bytecode = compile_to_bytecode(program)
  
  // Verify MOV instruction
  bytecode |> list.first |> should.equal(Ok(0xb7))
  
  // Verify EXIT instruction
  bytecode |> list.last |> should.equal(Ok(0x95))
}

5. Benchmark Performance

#[bench]
fn bench_program_execution(b: &mut Bencher) {
    let mut svm = LiteSVM::new();
    let program_data = load_bpf_program("../build/program.so");
    let program_id = Pubkey::new_unique();
    svm.add_program(program_id, &program_data);
    
    b.iter(|| {
        execute_program(&mut svm, program_id);
    });
}

Deployment Guidelines

1. Pre-Deployment Checklist

# Run all tests
gleam test

# Run LiteSVM integration tests
../scripts/test_litesvm.sh

# Verify ELF format
file program.so

# Check file size
ls -lh program.so

# Inspect bytecode
readelf -h program.so
xxd program.so | head -20

# Test on local validator
solana-test-validator
solana program deploy --url localhost program.so

# Test on devnet
solana program deploy --url devnet program.so

# Audit code
# - Check for security issues
# - Verify all inputs validated
# - Confirm proper error handling

2. Deployment Process

# Step 1: Choose network
solana config set --url https://api.devnet.solana.com
# or
solana config set --url https://api.mainnet-beta.solana.com

# Step 2: Check balance
solana balance

# Step 3: Deploy (with upgrade authority)
solana program deploy \
  --program-id program-keypair.json \
  --upgrade-authority upgrade-authority.json \
  program.so

# Step 4: Verify deployment
solana program show <PROGRAM_ID>

# Step 5: Test deployed program
# (Create and send test transactions)

3. Upgrade Strategy

# Deploy upgradeable program
solana program deploy \
  --upgradeable \
  program.so

# Later, upgrade the program
solana program upgrade \
  <PROGRAM_ID> \
  new_program.so

# For production: Transfer upgrade authority to multisig
solana program set-upgrade-authority \
  <PROGRAM_ID> \
  --new-upgrade-authority <MULTISIG_ADDRESS>

# Make program immutable (no more upgrades)
solana program set-upgrade-authority \
  <PROGRAM_ID> \
  --final

4. Monitoring

# Monitor program account
solana account <PROGRAM_ID>

# Check program size
solana program show <PROGRAM_ID>

# View transaction logs
solana logs --program <PROGRAM_ID>

Common Pitfalls

1. Forgetting EXIT Instruction

❌ Bad:

pub fn create_program() -> Expression {
  Return(IntLiteral(42))
  // Compiler adds EXIT, but be aware!
}

Good:

// The compiler handles this, but understanding is important
// Generated bytecode always includes EXIT at the end

2. Ignoring Compute Budget

// ❌ This could exceed compute budget!
pub fn expensive_loop() -> Expression {
  // Imagine a loop that does 1000 operations
  // Each operation costs compute units
  todo
}

// Keep operations reasonable
pub fn efficient_operation() -> Expression {
  // Simple, bounded operations
  Return(Add(IntLiteral(10), IntLiteral(5)))
}

3. Not Handling All Result Cases

❌ Bad:

pub fn process() {
  let Ok(value) = risky_operation()  // Panics on Error!
  value
}

Good:

pub fn process() -> Result(Int, String) {
  case risky_operation() {
    Ok(value) -> Ok(value)
    Error(e) -> Error("Failed to process: " <> e)
  }
}

4. Assuming Ordered Execution

// Remember: Solana transactions are atomic
// Either all instructions succeed or all fail
// But instruction order matters!

pub fn transfer_and_close() -> Result(Nil, String) {
  use _ <- result.try(transfer_tokens())
  use _ <- result.try(close_account())  // Must happen after transfer
  Ok(Nil)
}

5. Hardcoding Values

❌ Bad:

pub fn calculate_fee(amount: Int) -> Int {
  amount * 300 / 10000  // What is 300?
}

Good:

const fee_basis_points = 300  // 3% fee

pub fn calculate_fee(amount: Int) -> Int {
  amount * fee_basis_points / 10000
}

Design Patterns

1. State Machine Pattern

pub type State {
  Uninitialized
  Active(balance: Int)
  Frozen
  Closed
}

pub fn process_deposit(state: State, amount: Int) -> Result(State, String) {
  case state {
    Active(balance) -> Ok(Active(balance + amount))
    Frozen -> Error("Account is frozen")
    Closed -> Error("Account is closed")
    Uninitialized -> Error("Account not initialized")
  }
}

2. Builder Pattern

pub type SwapParams {
  SwapParams(
    input_mint: Option(String),
    output_mint: Option(String),
    amount: Option(Int),
    slippage: Option(Int),
  )
}

pub fn new_swap() -> SwapParams {
  SwapParams(
    input_mint: None,
    output_mint: None,
    amount: None,
    slippage: None,
  )
}

pub fn with_input_mint(params: SwapParams, mint: String) -> SwapParams {
  SwapParams(..params, input_mint: Some(mint))
}

pub fn build(params: SwapParams) -> Result(Swap, String) {
  // Validate all required fields are set
  todo
}

3. Validation Chain Pattern

pub fn validate_transfer(
  from: Account,
  to: Account,
  amount: Int,
) -> Result(Nil, String) {
  use _ <- result.try(validate_amount(amount))
  use _ <- result.try(validate_balance(from, amount))
  use _ <- result.try(validate_not_frozen(from))
  use _ <- result.try(validate_not_frozen(to))
  use _ <- result.try(validate_not_same_account(from, to))
  Ok(Nil)
}

4. Factory Pattern

pub fn create_token(token_type: TokenType) -> Result(Expression, String) {
  case token_type {
    Standard -> Ok(create_standard_token())
    Mintable -> Ok(create_mintable_token())
    Burnable -> Ok(create_burnable_token())
    Pausable -> Ok(create_pausable_token())
  }
}

Summary Checklist

Before deploying to mainnet:

  • All inputs validated
  • Integer overflow checks in place
  • Division by zero prevented
  • Account ownership verified
  • Signer authority checked
  • All error cases handled
  • Comprehensive tests written
  • Edge cases tested
  • LiteSVM integration tests passing
  • Tested on local validator
  • Tested on devnet
  • Code reviewed
  • Security audit completed (for high-value programs)
  • Compute budget analyzed
  • Performance optimized
  • Documentation complete
  • Upgrade strategy defined
  • Monitoring plan in place

Next: DeFi Tutorials