A comprehensive guide to writing secure, efficient, and maintainable Solana programs in Gleam.
- Security Best Practices
- Performance Optimization
- Code Quality
- Testing Strategies
- Deployment Guidelines
- Common Pitfalls
- Design Patterns
❌ 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")
}
}❌ 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")
}
}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")
}
}
}
}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")
}
}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")
}
}❌ 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")
}
}❌ 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))
}
}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
}❌ 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)))
}❌ 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)))
}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)Syscalls are expensive (100-1000 CU each).
// Group syscalls together
// Avoid calling same syscall multiple times
// Cache results when possible❌ Bad:
pub fn process(x, y) {
x + y
}Good:
pub fn process(x: Int, y: Int) -> Int {
x + y
}❌ 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
}❌ 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) { ... }❌ 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
}/// 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...
}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
}#[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());
}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
}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))
}#[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);
});
}# 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# 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)# 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# Monitor program account
solana account <PROGRAM_ID>
# Check program size
solana program show <PROGRAM_ID>
# View transaction logs
solana logs --program <PROGRAM_ID>❌ 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// ❌ 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)))
}❌ 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)
}
}// 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)
}❌ 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
}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")
}
}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
}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)
}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())
}
}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 →