Mollusk
Mollusk is a lightweight test harness for testing Solana programs. It provides a simple interface for testing Solana program instructions in a minified Solana Virtual Machine (SVM) environment. All test accounts must be explicitly defined, ensuring deterministic and repeatable tests.
Installation
Add mollusk-svm
as a dependency in Cargo.toml
:
$cargo add mollusk-svm --dev
[dev-dependencies]mollusk-svm = "0.5"
To benchmark compute unit usage, add mollusk-svm-bencher
as dependency in
Cargo.toml
:
$cargo add mollusk-svm-bencher --dev
[dev-dependencies]mollusk-svm-bencher = "0.5"
To use the token program, token2022 program (token extensions), and associated
token program for testing with Mollusk, add mollusk-svm-programs-token
as
dependency in Cargo.toml
:
$cargo add mollusk-svm-programs-token --dev
[dev-dependencies]mollusk-svm-programs-token = "0.5"
Mollusk SVM
The following example shows a minimal setup for testing a basic Solana program using Mollusk.
Hello World Program
This example demonstrates how to test a basic Solana program using Mollusk. The program simply prints "Hello, world!" to the program logs when invoked.
Running cargo build-sbf
generates the compiled program at
/target/deploy/<program_name>.so
.
use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(_program_id: &Pubkey,_accounts: &[AccountInfo],_instruction_data: &[u8],) -> ProgramResult {msg!("Hello, world!");Ok(())}#[cfg(test)]mod tests {use mollusk_svm::{result::Check, Mollusk};use solana_sdk::{instruction::Instruction, pubkey::Pubkey};#[test]fn test_hello_world() {let program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/hello_world");let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);}}
To test a Solana program with Mollusk:
- Create a
Mollusk
instance - Initialize Mollusk with a program ID and the path to the compiled program (.so
file) - Build an instruction - Create an instruction to invoke the program
- Process and validate - Process the instruction using Mollusk and validate the result
#[cfg(test)]mod tests {use mollusk_svm::{result::Check, Mollusk};use solana_sdk::{instruction::Instruction, pubkey::Pubkey};#[test]fn test_hello_world() {let program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/hello_world");let instruction = Instruction::new_with_bytes(program_id, &[], vec![]);mollusk.process_and_validate_instruction(&instruction, &[], &[Check::success()]);}}
To execute the test, run cargo test
.
When the test runs successfully, you'll see output similar to the following:
running 1 test[2025-09-22T19:25:50.427685000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs invoke [1][2025-09-22T19:25:50.429669000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, world![2025-09-22T19:25:50.429690000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs consumed 211 of 1400000 compute units[2025-09-22T19:25:50.429726000Z DEBUG solana_runtime::message_processor::stable_log] Program 11157t3sqMV725NVRLrVQbAu98Jjfk1uCKehJnXXQs successtest tests::test_hello_world ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02sDoc-tests hello_world
The
Mollusk
struct provides a simple interface for testing Solana programs. All fields can
be manipulated through a handful of helper methods, but users can also directly
access and modify them if they desire more control.
To initialize Mollusk with a default instance use the Mollusk::default
method.
// Default instance with no custom programslet mollusk = Mollusk::default();
To initialize Mollusk with a specific program use the Mollusk::new
method.
// Initialize Mollusk with a specific program from a file pathlet program_id = Pubkey::new_unique();let mollusk = Mollusk::new(&program_id, "target/deploy/my_program");
To add a program to Mollusk use the Mollusk::add_program
method.
let mollusk = Mollusk::default();let program_id = Pubkey::new_unique();// Add a program to Molluskmollusk.add_program(&program_id,"target/deploy/my_program",&bpf_loader_upgradeable::id(),);
When providing the file path, do not include the .so
extension. For example,
"path/to/my_program"
is correct, but "path/to/my_program.so"
is not.
Processing Instructions
Mollusk provides four main methods for processing instructions:
Method | Description |
---|---|
process_instruction | Process an instruction and return the result. |
process_and_validate_instruction | Process an instruction and perform a series of checks on the result, panicking if any checks fail. |
process_instruction_chain | Process multiple instructions and return the result. |
process_and_validate_instruction_chain | Process multiple instructions and perform a series of checks on each result, panicking if any checks fail. |
The
InstructionResult
contains the details of a processed instruction.
Single Instruction
Use the process_instruction
method to process a single instruction without
checks on the result. You can manually validate the results after processing.
pub fn process_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],) -> InstructionResult
The following example processes a SOL transfer instruction without validation checks.
The examples below run Mollusk in the main
function for demonstration
purposes. In practice, you'll typically use Mollusk in a test module annotated
with the #[test]
attribute.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Set up accountslet sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();let initial_lamports = 1_000_000;let transfer_amount = 250_000;// Create transfer instructionlet instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);// Define initial account stateslet accounts = vec![(sender,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(recipient,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Process the instructionlet result = mollusk.process_instruction(&instruction, &accounts);println!("{:#?}", result);// Check the resultassert!(result.program_result.is_ok());assert_eq!(result.get_account(&sender).unwrap().lamports, 750_000);assert_eq!(result.get_account(&recipient).unwrap().lamports, 250_000);}
Single Instruction with Checks
Use the process_and_validate_instruction
method to process a single
instruction with validation checks. This method will panic if any check fails.
pub fn process_and_validate_instruction(&self,instruction: &Instruction,accounts: &[(Pubkey, Account)],checks: &[Check],) -> InstructionResult
The following example processes a SOL transfer instruction with validation checks.
use {mollusk_svm::{Mollusk, result::Check},solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {let mollusk = Mollusk::default();let sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();let initial_lamports = 1_000_000;let transfer_amount = 250_000;let instruction = system_instruction::transfer(&sender, &recipient, transfer_amount);let accounts = vec![(sender,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(recipient,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Define validation checkslet checks = vec![Check::success(),Check::account(&sender).lamports(750_000).build(),Check::account(&recipient).lamports(250_000).build(),];// Process and validate (will panic if any check fails)let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);println!("{:#?}", result);}
Multiple Instructions
Use the process_instruction_chain
method to process multiple instructions
sequentially without validation checks.
pub fn process_instruction_chain(&self,instructions: &[Instruction],accounts: &[(Pubkey, Account)],) -> InstructionResult
The following example processes two SOL transfer instructions without validation checks.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {let mollusk = Mollusk::default();// Set up accountslet alice = Pubkey::new_unique();let bob = Pubkey::new_unique();let charlie = Pubkey::new_unique();let initial_lamports = 1_000_000;// Create chain of transferslet instructions = vec![system_instruction::transfer(&alice, &bob, 300_000), // Alice -> Bobsystem_instruction::transfer(&bob, &charlie, 100_000), // Bob -> Charlie];let accounts = vec![(alice,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(bob,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(charlie,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Process the instruction chainlet result = mollusk.process_instruction_chain(&instructions, &accounts);println!("{:#?}", result);// Final balances: Alice=700K, Bob=200K, Charlie=100Kassert_eq!(result.get_account(&alice).unwrap().lamports, 700_000);assert_eq!(result.get_account(&bob).unwrap().lamports, 200_000);assert_eq!(result.get_account(&charlie).unwrap().lamports, 100_000);}
Multiple Instructions with Checks
Use the process_and_validate_instruction_chain
method to process multiple
instructions with validation checks after each instruction. Each instruction has
its own set of checks that must pass.
pub fn process_and_validate_instruction_chain(&self,instructions: &[(&Instruction, &[Check])],accounts: &[(Pubkey, Account)],) -> InstructionResult
The following example processes a chain of two SOL transfer instructions with validation checks after each instruction.
use {mollusk_svm::{result::Check, Mollusk},solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {let mollusk = Mollusk::default();// Create accountslet alice = Pubkey::new_unique();let bob = Pubkey::new_unique();let charlie = Pubkey::new_unique();let initial_lamports = 1_000_000;// Create transfer instructionslet transfer1 = system_instruction::transfer(&alice, &bob, 300_000);let transfer2 = system_instruction::transfer(&bob, &charlie, 100_000);// Initial accountslet accounts = vec![(alice,Account {lamports: initial_lamports,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(bob,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(charlie,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Define checks for each instructionlet checks_after_transfer1 = vec![Check::success(),Check::account(&alice).lamports(700_000) // 1M - 300K.build(),Check::account(&bob).lamports(300_000) // 0 + 300K.build(),Check::account(&charlie).lamports(0) // Unchanged.build(),];let checks_after_transfer2 = vec![Check::success(),Check::account(&alice).lamports(700_000) // Unchanged from previous.build(),Check::account(&bob).lamports(200_000) // 300K - 100K.build(),Check::account(&charlie).lamports(100_000) // 0 + 100K.build(),];// Process with validation at each steplet instruction_and_checks = [(&transfer1, checks_after_transfer1.as_slice()),(&transfer2, checks_after_transfer2.as_slice()),];// Execute chain (panics if any check fails)let result = mollusk.process_and_validate_instruction_chain(&instruction_and_checks, &accounts);println!("{:#?}", result);}
Validation Checks
Mollusk provides a set of helper methods to check the results of a processed instruction.
use mollusk_svm::result::Check;
Use the following methods to validate instruction results:
// Program execution succeededCheck::success()// Program returned specific errorCheck::err(ProgramError::InvalidArgument)// Instruction level errorCheck::instruction_err(InstructionError::InsufficientFunds)// Check with specific program resultCheck::program_result(ProgramResult::Success)// Compute units consumedCheck::compute_units(1000)// Execution timeCheck::time(100)// Return data from instruction executionCheck::return_data(&[1, 2, 3, 4])
Use the following to validate Account states:
// Single account validationCheck::account(&pubkey).lamports(1_000_000) // Exact lamports.owner(&program_id) // Account owner.data(&expected_data) // Exact data match.data_slice(8, &[1, 2, 3]) // Partial data match at offset.executable(false) // Executable flag.space(100) // Account data size.closed() // Account is closed (0 lamports).rent_exempt() // Account is rent-exempt.build()// Check all accounts are rent exemptCheck::all_rent_exempt()
Persistent Account State
The
MolluskContext
is a wrapper around Mollusk
that maintains account state across multiple
instruction calls through its account_store
. The methods for processing
instructions are identical to Mollusk
.
Unlike Mollusk
, which requires passing accounts
to each method (e.g.
process_instruction
), MolluskContext
manages accounts internally through its
account_store
. This eliminates the need for the accounts
parameter when
processing instructions.
Create an account_store
using the with_context
method:
use std::collections::HashMap;use solana_sdk::{account::Account, pubkey::Pubkey, system_program};use mollusk_svm::Mollusk;let mollusk = Mollusk::default();let account_address = Pubkey::new_unique();let mut account_store = HashMap::new();account_store.insert(account_address,Account {lamports: 1_000_000,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},);let context = mollusk.with_context(account_store);
The following example processes two separate SOL transfer instructions with
persistent account state between the instructions through the account_store
.
use {mollusk_svm::Mollusk,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},std::collections::HashMap,};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create accountslet sender = Pubkey::new_unique();let recipient = Pubkey::new_unique();// Create account store with initial balanceslet mut account_store = HashMap::new();account_store.insert(sender,Account {lamports: 1_000_000,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},);account_store.insert(recipient,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},);// Create a stateful contextlet context = mollusk.with_context(account_store);// First transfer: 200,000 lamportslet instruction1 = system_instruction::transfer(&sender, &recipient, 200_000);context.process_instruction(&instruction1);// Second transfer: 100,000 lamports (state persists from first transfer)let instruction2 = system_instruction::transfer(&sender, &recipient, 100_000);context.process_instruction(&instruction2);// Check final balanceslet store = context.account_store.borrow();let sender_account = store.get(&sender).unwrap();let recipient_account = store.get(&recipient).unwrap();println!("Sender: {:#?}", sender_account);println!("Recipient: {:#?}", recipient_account);}
Mollusk Sysvars
Mollusk provides a custom
Sysvars
struct for modifying its values for testing.
Use the warp_to_slot
method to update the sysvar clock to simulate moving
forward or backward in time to a specific slot.
use mollusk_svm::Mollusk;fn main() {// Initialize Mollusklet mut mollusk = Mollusk::default();// Show initial slotprintln!("Initial slot: {}", mollusk.sysvars.clock.slot);// Warp to slot 1000mollusk.warp_to_slot(100);println!("After warp: {}", mollusk.sysvars.clock.slot);// Warp to slot 10mollusk.warp_to_slot(10);println!("After second warp: {}", mollusk.sysvars.clock.slot);}
The following example shows how to modify the Mollusk sysvar directly by
accessing the sysvars
field to change the rent parameters. You can modify
other sysvar values the same way.
use {mollusk_svm::Mollusk, solana_sdk::rent::Rent};fn main() {let mut mollusk = Mollusk::default();// Show default rentprintln!("Default rent exemption for 1000 bytes: {} lamports",mollusk.sysvars.rent.minimum_balance(1000));// Customize rent parametersmollusk.sysvars.rent = Rent {lamports_per_byte_year: 1,exemption_threshold: 1.0,burn_percent: 0,};// Show custom rentprintln!("Custom rent exemption for 1000 bytes: {} lamports",mollusk.sysvars.rent.minimum_balance(1000));}
Compute Unit Benchmarking
MolluskComputeUnitBencher
tracks compute unit usage of a program's instructions. Results are written to a
markdown file.
Requires
mollusk-svm-bencher
as a dependency.
The following example benchmarks the compute unit usage of a SOL transfer instruction.
use {mollusk_svm::Mollusk,mollusk_svm_bencher::MolluskComputeUnitBencher,solana_sdk::{account::Account, pubkey::Pubkey, system_instruction, system_program},};fn main() {// Initialize Mollusklet mollusk = Mollusk::default();// Create test accountslet sender = Pubkey::new_unique();let receiver = Pubkey::new_unique();// Transfer instructionlet transfer = system_instruction::transfer(&sender, &receiver, 100_000);let accounts = vec![(sender,Account {lamports: 1_000_000,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),(receiver,Account {lamports: 0,data: vec![],owner: system_program::id(),executable: false,rent_epoch: 0,},),];// Run benchmarkMolluskComputeUnitBencher::new(mollusk).bench(("transfer", &transfer, &accounts)).must_pass(true).out_dir("./target/benches").execute();}
The benchmark results are written to the specified out_dir
as a markdown file
named compute_units.md
.
#### 2025-09-19 22:28:53.691839 UTCSolana CLI Version: solana-cli 2.2.20 (src:dabc99a5; feat:3073396398,client:Agave)| Name | CUs | Delta || -------- | --- | ------- || transfer | 150 | - new - |
Token Program Testing
Use the
mollusk-svm-programs-token
crate add the token program, token2022 program (token extensions), and
associated token program to Mollusk for testing.
use {mollusk_svm::Mollusk,mollusk_svm_programs_token::{associated_token, token, token2022},};let mut mollusk = Mollusk::default();// Add SPL Token Programtoken::add_program(&mut mollusk);// Add SPL Token-2022 Programtoken2022::add_program(&mut mollusk);// Add Associated Token Account Programassociated_token::add_program(&mut mollusk);
The following example demonstrates testing a token transfer using Mollusk.
The example below manually defines the test accounts for demonstration
purposes. The mollusk-svm-programs-token
also includes helper functions to
create the mint and token accounts.
use {mollusk_svm::{result::Check, Mollusk},mollusk_svm_programs_token::token,solana_sdk::{account::Account, program_pack::Pack, pubkey::Pubkey},spl_token::{instruction::transfer_checked,state::{Account as TokenAccount, Mint},},};fn main() {// Initialize Mollusk with Token programlet mut mollusk = Mollusk::default();token::add_program(&mut mollusk);// Create account keyslet mint = Pubkey::new_unique();let source = Pubkey::new_unique();let destination = Pubkey::new_unique();let authority = Pubkey::new_unique();// Token configurationlet decimals = 6;let transfer_amount = 1_000_000; // 1 token with 6 decimalslet initial_balance = 10_000_000; // 10 tokens// Calculate rent-exempt minimumslet mint_rent = mollusk.sysvars.rent.minimum_balance(Mint::LEN);let account_rent = mollusk.sysvars.rent.minimum_balance(TokenAccount::LEN);// Create mint accountlet mut mint_data = vec![0u8; Mint::LEN];Mint::pack(Mint {mint_authority: Some(authority).into(),supply: initial_balance,decimals,is_initialized: true,freeze_authority: None.into(),},&mut mint_data,).unwrap();// Create source token accountlet mut source_data = vec![0u8; TokenAccount::LEN];TokenAccount::pack(TokenAccount {mint,owner: authority,amount: initial_balance,delegate: None.into(),state: spl_token::state::AccountState::Initialized,is_native: None.into(),delegated_amount: 0,close_authority: None.into(),},&mut source_data,).unwrap();// Create destination token accountlet mut destination_data = vec![0u8; TokenAccount::LEN];TokenAccount::pack(TokenAccount {mint,owner: Pubkey::new_unique(),amount: 0,delegate: None.into(),state: spl_token::state::AccountState::Initialized,is_native: None.into(),delegated_amount: 0,close_authority: None.into(),},&mut destination_data,).unwrap();// Setup accounts for transfer_checkedlet accounts = vec![(source,Account {lamports: account_rent,data: source_data,owner: token::ID,executable: false,rent_epoch: 0,},),(mint,Account {lamports: mint_rent,data: mint_data,owner: token::ID,executable: false,rent_epoch: 0,},),(destination,Account {lamports: account_rent,data: destination_data,owner: token::ID,executable: false,rent_epoch: 0,},),(authority,Account {lamports: 1_000_000,data: vec![],owner: Pubkey::default(),executable: false,rent_epoch: 0,},),];// Create transfer_checked instructionlet instruction = transfer_checked(&token::ID,&source,&mint,&destination,&authority,&[],transfer_amount,decimals,).unwrap();// Expected balances after transferlet expected_source_balance = (initial_balance - transfer_amount).to_le_bytes();let expected_dest_balance = transfer_amount.to_le_bytes();// Define validation checkslet checks = vec![Check::success(),Check::account(&source).data_slice(64, &expected_source_balance) // Token amount is at offset 64.build(),Check::account(&destination).data_slice(64, &expected_dest_balance).build(),];// Process and validate the instructionlet result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks);println!("{:#?}", result);// Deserialize token account datalet source_account = result.get_account(&source).unwrap();let source_token = TokenAccount::unpack(&source_account.data).unwrap();println!("Source Token Account: {:#?}", source_token);let destination_account = result.get_account(&destination).unwrap();let dest_token = TokenAccount::unpack(&destination_account.data).unwrap();println!("Destination Token Account: {:#?}", dest_token);}
Is this page helpful?