Developing smart contracts on the Solana blockchain offers high performance and low transaction costs, making it a top choice for decentralized application (dApp) builders. As the fifth-largest blockchain by market capitalization, Solana supports a rapidly growing ecosystem. However, unlike Ethereum-based (EVM) chains that use domain-specific languages like Solidity, Solana programs—often referred to as on-chain programs—are typically written in Rust, a systems programming language known for speed and safety.
This steep learning curve can be challenging, especially for developers accustomed to EVM environments. Additionally, Solana introduces unique concepts such as program-derived addresses (PDA), rent-exempt accounts, and stateless programs, which require adjustment from traditional smart contract paradigms.
This guide walks you through the essential steps of building a basic escrow smart contract on Solana—from environment setup to local deployment and testing—based on foundational principles from well-documented resources.
👉 Discover how to launch your Solana dApp with powerful tools and fast execution.
Setting Up Your Development Environment
Before writing any code, ensure your development environment is properly configured.
Install Rust
Solana smart contracts are primarily developed using Rust. Begin by installing the latest version:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shAfter installation, verify the version:
rustc -VInstall Solana CLI Tools
Next, install the Solana command-line toolkit:
sh -c "$(curl -sSfL https://release.solana.com/v1.9.1/install)"Verify the installation:
solana -VThese tools allow you to interact with the Solana network, deploy programs, and manage wallets.
Creating a New Rust Project
Use Cargo, Rust’s package manager, to scaffold a new library project:
cargo new escrow --libThen create an Xargo.toml file in the root directory with the following content:
[target.bpfel-unknown-unknown.dependencies.std]
features = []This configuration ensures compatibility when compiling for Solana’s BPF (Berkeley Packet Filter) runtime.
Update your Cargo.toml with necessary dependencies:
[package]
name = "escrow"
version = "0.1.0"
edition = "2021"
[features]
no-entrypoint = []
[dependencies]
arrayref = "0.3.6"
solana-program = "1.7.11"
thiserror = "1.0"
spl-token = { version = "3.1.1", features = ["no-entrypoint"] }
[lib]
crate-type = ["cdylib", "lib"]These dependencies include core Solana program libraries and support for token operations via the SPL Token standard.
Understanding the Escrow Use Case
Smart contracts on Solana act as trustless intermediaries—ideal for escrow scenarios where two parties, Alice and Bob, wish to exchange assets without mutual trust.
In this model:
- Alice wants to trade her token X for Bob’s token Y.
- She creates a temporary account to hold X and transfers ownership to a program-controlled address (a PDA).
- The contract records her expected amount of Y.
- When Bob fulfills the terms, he receives X, and Alice gets Y.
Unlike EVM chains, Solana programs are stateless—they don’t store data internally. All persistent data resides in separate accounts, specifically within their data field.Note: While Solana contracts can be upgradeable (unless disabled), transparency tools like on-chain source verification are still limited. Always audit deployed programs carefully.
Building the Contract Structure
We'll break down the program into modular components following Rust best practices.
4.1 lib.rs – Main Module Entry
Cargo automatically generates src/lib.rs. Replace its contents and organize module declarations:
pub mod error;
pub mod instruction;
pub mod state;
pub mod processor;
#[cfg(not(feature = "no-entrypoint"))]
mod entrypoint;Remove any default unit tests to avoid confusion.
4.2 error.rs – Custom Error Handling
Create src/error.rs to define custom errors:
use thiserror::Error;
use solana_program::program_error::ProgramError;
#[derive(Error, Debug, Copy, Clone)]
pub enum EscrowError {
#[error("Invalid Instruction")]
InvalidInstruction,
#[error("Not Rent Exempt")]
NotRentExempt,
}
impl From<EscrowError> for ProgramError {
fn from(e: EscrowError) -> Self {
ProgramError::Custom(e as u32)
}
}Using thiserror simplifies error trait implementation and improves readability.
4.3 instruction.rs – Parsing Input Data
Create src/instruction.rs to handle instruction deserialization:
use std::convert::TryInto;
use crate::error::EscrowError;
use solana_program::program_error::ProgramError;
pub enum EscrowInstruction {
InitEscrow { amount: u64 }
}
impl EscrowInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let (tag, rest) = input.split_first().ok_or(EscrowError::InvalidInstruction)?;
Ok(match tag {
0 => Self::InitEscrow {
amount: Self::unpack_amount(rest)?,
},
_ => return Err(EscrowError::InvalidInstruction.into()),
})
}
fn unpack_amount(input: &[u8]) -> Result<u64, ProgramError> {
let amount = input
.get(..8)
.and_then(|s| s.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(EscrowError::InvalidInstruction)?;
Ok(amount)
}
}This module parses incoming transaction data into executable instructions.
4.4 processor.rs – Core Logic Handler
Create src/processor.rs to process instructions:
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
sysvar::{rent::Rent, Sysvar},
program::invoke,
};
use crate::{
instruction::EscrowInstruction,
error::EscrowError,
state::Escrow,
program_pack::{Pack, IsInitialized},
};
pub struct Processor;
impl Processor {
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = EscrowInstruction::unpack(instruction_data)?;
match instruction {
EscrowInstruction::InitEscrow { amount } => {
msg!("Instruction: InitEscrow");
Self::process_init_escrow(accounts, amount, program_id)
}
}
}
fn process_init_escrow(
accounts: &[AccountInfo],
amount: u64,
program_id: &Pubkey,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let initializer = next_account_info(account_iter)?;
if !initializer.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let temp_token_account = next_account_info(account_iter)?;
let token_to_receive_account = next_account_info(account_iter)?;
if *token_to_receive_account.owner != spl_token::id() {
return Err(ProgramError::IncorrectProgramId);
}
let escrow_account = next_account_info(account_iter)?;
let rent = Rent::from_account_info(next_account_info(account_iter)?)?;
if !rent.is_exempt(escrow_account.lamports(), escrow_account.data_len()) {
return Err(EscrowError::NotRentExempt.into());
}
let mut escrow_info = Escrow::unpack_unchecked(&escrow_account.try_borrow_data()?)?;
if escrow_info.is_initialized() {
return Err(ProgramError::AccountAlreadyInitialized);
}
escrow_info.is_initialized = true;
escrow_info.initializer_pubkey = *initializer.key;
escrow_info.temp_token_account_pubkey = *temp_token_account.key;
escrow_info.initializer_token_to_receive_account_pubkey = *token_to_receive_account.key;
escrow_info.expected_amount = amount;
Escrow::pack(escrow_info, &mut escrow_account.try_borrow_mut_data()?)?;
let (pda, _bump_seed) = Pubkey::find_program_address(&[b"escrow"], program_id);
let token_program = next_account_info(account_iter)?;
let owner_change_ix = spl_token::instruction::set_authority(
token_program.key,
temp_token_account.key,
Some(&pda),
spl_token::instruction::AuthorityType::AccountOwner,
initializer.key,
&[&initializer.key],
)?;
msg!("Calling token program to transfer ownership...");
invoke(&owner_change_ix, &[
temp_token_account.clone(),
initializer.clone(),
token_program.clone(),
])?;
Ok(())
}
}This processor validates inputs, updates state, and invokes SPL Token instructions securely.
4.5 entrypoint.rs – Program Entry Point
Create src/entrypoint.rs:
use crate::processor::Processor;
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Processor::process(program_id, accounts, instruction_data)
}The entrypoint! macro marks this function as the on-chain entry.
4.6 state.rs – Data Serialization Layer
Create src/state.rs to define and serialize contract state:
use solana_program::{
program_pack::{IsInitialized, Pack, Sealed},
program_error::ProgramError,
pubkey::Pubkey,
};
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
pub struct Escrow {
pub is_initialized: bool,
pub initializer_pubkey: Pubkey,
pub temp_token_account_pubkey: Pubkey,
pub initializer_token_to_receive_account_pubkey: Pubkey,
pub expected_amount: u64,
}
impl Sealed for Escrow {}
impl IsInitialized for Escrow {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
impl Pack for Escrow {
const LEN: usize = 105;
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
let src = array_ref![src, 0, 105];
let (
is_initialized,
initializer_pubkey,
temp_token_account_pubkey,
initializer_token_to_receive_account_pubkey,
expected_amount,
) = array_refs![src, 1, 32, 32, 32, 8];
let is_initialized = match is_initialized {
[0] => false,
[1] => true,
_ => return Err(ProgramError::InvalidAccountData),
};
Ok(Escrow {
is_initialized,
initializer_pubkey: Pubkey::new_from_array(*initializer_pubkey),
temp_token_account_pubkey: Pubkey::new_from_array(*temp_token_account_pubkey),
initializer_token_to_receive_account_pubkey: Pubkey::new_from_array(*initializer_token_to_receive_account_pubkey),
expected_amount: u64::from_le_bytes(*expected_amount),
})
}
fn pack_into_slice(&self, dst: &mut [u8]) {
let dst = array_mut_ref![dst, 0, 105];
let (
is_initialized_dst,
initializer_pubkey_dst,
temp_token_account_pubkey_dst,
initializer_token_to_receive_account_pubkey_dst,
expected_amount_dst,
) = mut_array_refs![dst, 1, 32, 32, 32, 8];
*is_initialized_dst = self.is_initialized as u8;
initializer_pubkey_dst.copy_from_slice(self.initializer_pubkey.as_ref());
temp_token_account_pubkey_dst.copy_from_slice(self.temp_token_account_pubkey.as_ref());
initializer_token_to_receive_account_pubkey_dst.copy_from_slice(self.initializer_token_to_receive_account_pubkey.as_ref());
*expected_amount_dst = self.expected_amount.to_le_bytes();
}
}This ensures efficient serialization and alignment with Solana's memory model.
Compiling and Deploying Locally
Step 1: Compile the Program
Run:
cargo build-bpf --manifest-path=./Cargo.toml --bpf-out-dir=dist/programStep 2: Start Local Validator
solana-test-validatorStep 3: Configure CLI
Ensure your CLI points to localhost:
solana config set --url http://localhost:8899Check balance:
solana balanceYou should see test SOL for transactions.
Step 4: Deploy
Use the output .so file path:
solana program deploy ./dist/program/escrow.soSave the resulting program ID—you'll need it in tests.
👉 Deploy your first Solana contract with confidence using advanced dev tools.
Testing the Escrow Contract
Write integration tests in JavaScript using @solana/web3.js.
Install dependencies:
yarn add @solana/web3.js @solana/spl-token buffer-layout bn.jsCreate test scripts to:
- Airdrop SOL
- Mint test tokens (X and Y)
- Create associated token accounts
- Initialize escrow state
- Simulate trades
Example assertions:
- Verify account initialization status
- Confirm token ownership transfer
- Check PDA derivation correctness
For full implementation details, refer to the complete test suite structure outlined in the original tutorial.
Frequently Asked Questions (FAQ)
Q: Why does Solana use Rust instead of Solidity?
A: Rust provides memory safety and high performance critical for Solana’s high-throughput architecture. It enables efficient execution within Solana’s BPF runtime.
Q: Are Solana smart contracts upgradeable?
A: Yes—by default, programs can be upgraded unless explicitly locked during deployment. This allows bug fixes but requires trust in deployers.
Q: How is data stored in Solana contracts?
A: Contracts themselves are stateless. All data lives in separate accounts' data fields and must be serialized manually using Pack.
Q: What is a PDA (Program Derived Address)?
A: A PDA is a key derived from a program ID and seeds, allowing programs to “own” accounts without holding private keys—essential for secure cross-program invocations.
Q: What does "rent-exempt" mean?
A: To prevent account bloat, Solana charges storage rent unless an account holds enough SOL to be “rent-exempt.” This minimum depends on data size.
Q: Can I verify deployed contract source code?
A: Currently, Solana lacks built-in source verification like Etherscan. Third-party tools are emerging, but full transparency isn’t guaranteed.
Ready to go further? Explore advanced patterns like atomic swaps, staking pools, or NFT marketplaces built on Solana’s high-speed foundation.
👉 Accelerate your blockchain journey with seamless wallet integration and real-time analytics.