Architecture Overview

Learn about the architecture of a liquid staking protocol on Solana.

Last updated: 2025-02-20
Edit on GitHub

Introduction

This guide provides a detailed overview of the architecture required to build a secure and efficient liquid staking protocol on Solana. Understanding these components and their interactions is crucial for implementing a robust protocol.

Protocol Requirements
Before diving into implementation, ensure you have a solid understanding of Solana's architecture, Rust programming, and stake pool mechanics. This guide assumes familiarity with these concepts.

System Overview

A liquid staking protocol on Solana consists of several interconnected components working together to provide secure staking services while maintaining liquidity through tokenization.

System Architecture Diagram

Diagram: High-level architecture of the liquid staking protocol

On-Chain Components

  • Stake Pool Program
  • SPL Token Program
  • Validator List Program
  • Oracle Program (optional)

Off-Chain Services

  • Validator Monitor
  • Reward Distributor
  • Analytics Engine
  • Client SDK

Key Interactions

Protocol Initialization

1import { Connection, Keypair, PublicKey } from '@solana/web3.js'
2import { StakePool } from './stake-pool'
3
4async function initializeProtocol() {
5  // Initialize connection
6  const connection = new Connection('https://api.mainnet-beta.solana.com')
7  
8  // Generate protocol authority
9  const authority = Keypair.generate()
10  
11  // Create stake pool
12  const stakePool = await StakePool.create(
13    connection,
14    authority,
15    {
16      epochFee: { numerator: 1, denominator: 100 }, // 1% fee
17      validatorList: new PublicKey('...'),
18      maxValidators: 100,
19      minStake: 1_000_000_000, // 1 SOL
20    }
21  )
22  
23  // Initialize validator list
24  await stakePool.initializeValidatorList()
25  
26  return stakePool
27}
Design Considerations
When designing your protocol, consider factors like validator selection criteria, fee structure, and reward distribution mechanism. These choices will impact your protocol's efficiency and attractiveness to users.

Core Components

The liquid staking protocol is built on several core components that work together to provide secure and efficient staking services. Understanding these components is crucial for successful implementation.

1. Stake Pool Program

The central program managing stake accounts and token minting.

Stake Pool State

1#[derive(Clone, Debug, Default, PartialEq)]
2pub struct StakePool {
3    /// Protocol version
4    pub version: u8,
5    
6    /// Manager authority
7    pub manager: Pubkey,
8    
9    /// Staker authority
10    pub staker: Pubkey,
11    
12    /// Stake pool withdraw authority
13    pub withdraw_authority: Pubkey,
14    
15    /// Validator list storage account
16    pub validator_list: Pubkey,
17    
18    /// Reserve stake account
19    pub reserve_stake: Pubkey,
20    
21    /// Pool Mint account
22    pub pool_mint: Pubkey,
23    
24    /// Manager fee account
25    pub manager_fee_account: Pubkey,
26    
27    /// Pool token program id
28    pub token_program_id: Pubkey,
29    
30    /// Total stake under management
31    pub total_lamports: u64,
32    
33    /// Total supply of pool tokens
34    pub pool_token_supply: u64,
35    
36    /// Last epoch stake pool was updated
37    pub last_update_epoch: u64,
38    
39    /// Fee applied to deposits
40    pub deposit_fee: Fee,
41    
42    /// Fee applied to withdrawals
43    pub withdrawal_fee: Fee,
44    
45    /// Fee charged on stake rewards
46    pub reward_fee: Fee,
47}

2. Validator List

Manages the list of approved validators and their stake accounts.

Validator List Structure

1#[derive(Clone, Debug, Default, PartialEq)]
2pub struct ValidatorList {
3    /// Protocol version
4    pub version: u8,
5    
6    /// Maximum number of validators
7    pub max_validators: u32,
8    
9    /// List of stake info for each validator
10    pub validators: Vec<ValidatorStakeInfo>,
11}
12
13#[derive(Clone, Debug, Default, PartialEq)]
14pub struct ValidatorStakeInfo {
15    /// Validator vote account address
16    pub vote_account_address: Pubkey,
17    
18    /// Active stake amount
19    pub active_stake_lamports: u64,
20    
21    /// Transient stake amount
22    pub transient_stake_lamports: u64,
23    
24    /// Last update epoch
25    pub last_update_epoch: u64,
26    
27    /// Validator status
28    pub status: ValidatorStatus,
29    
30    /// Validator score (optional)
31    pub score: Option<u32>,
32}

3. Token Management

Handles liquid staking token (stSOL) minting and burning.

Token Management

1import { Token, TOKEN_PROGRAM_ID, MintLayout } from '@solana/spl-token'
2
3async function createStakeToken(
4  connection: Connection,
5  payer: Keypair,
6  authority: Keypair
7) {
8  // Create mint account
9  const mint = await Token.createMint(
10    connection,
11    payer,
12    authority.publicKey,
13    authority.publicKey,
14    9, // 9 decimals to match SOL
15    TOKEN_PROGRAM_ID
16  )
17
18  // Create pool token account
19  const tokenAccount = await mint.createAccount(payer.publicKey)
20
21  return {
22    mint,
23    tokenAccount,
24  }
25}

4. Reward Distribution

Manages stake rewards collection and distribution.

Reward Distribution

1pub fn process_distribute_rewards(
2    program_id: &Pubkey,
3    accounts: &[AccountInfo],
4) -> ProgramResult {
5    let account_info_iter = &mut accounts.iter();
6    
7    // Get accounts
8    let stake_pool = next_account_info(account_info_iter)?;
9    let validator_list = next_account_info(account_info_iter)?;
10    let pool_mint = next_account_info(account_info_iter)?;
11    let reward_distributor = next_account_info(account_info_iter)?;
12    
13    // Calculate rewards
14    let rewards = calculate_validator_rewards(stake_pool, validator_list)?;
15    
16    // Update pool token supply to reflect rewards
17    update_pool_token_supply(pool_mint, rewards)?;
18    
19    // Distribute rewards to token holders
20    distribute_to_token_holders(rewards)?;
21    
22    Ok(())
23}
Security Considerations
Each component must implement proper access controls and validation checks. Pay special attention to authority verification and state transitions.

Data Flow & State Management

Understanding how data flows through your liquid staking protocol is crucial for maintaining consistency and preventing state corruption. The diagram below illustrates the key interactions and data flow between components.

User WalletStake PoolCentral ManagementToken MintstSOLValidator ListSelection & ManagementValidatorsStaking & Rewards1. Deposit SOL2. Mint stSOL3. Receive stSOL4. Select Validators5. Delegate Stake6. RewardsData Flow: SOL Deposit → stSOL Minting → Validator Selection → Staking → Reward Distribution

Key Data Flows

  • 1.
    SOL Deposit

    Users deposit SOL into the stake pool for staking

  • 2.
    Token Minting

    Stake pool triggers minting of stSOL tokens

  • 3.
    Liquid Token Distribution

    Users receive stSOL representing their staked SOL

  • 4.
    Validator Selection

    Protocol manages validator selection based on performance criteria

  • 5.
    Stake Delegation

    SOL is delegated to selected validators

  • 6.
    Reward Distribution

    Staking rewards flow back to the stake pool for distribution

State Management
Each data flow represents a state transition that must be carefully managed and validated to ensure protocol security and consistency.

Security Model

Security is paramount in liquid staking protocols. This section outlines the key security considerations and implementation patterns to protect user funds and ensure protocol stability.

1. Access Control

Implement robust access control mechanisms to protect sensitive operations.

Authority Verification

1pub fn verify_authority(
2    authority: &AccountInfo,
3    expected: &Pubkey,
4    authority_type: AuthorityType,
5) -> ProgramResult {
6    // Verify authority is a signer
7    if !authority.is_signer {
8        return Err(StakePoolError::SignatureMissing.into());
9    }
10
11    // Verify authority matches expected
12    if authority.key != expected {
13        return Err(match authority_type {
14            AuthorityType::Staker =>
15                StakePoolError::InvalidStakePoolAuthority.into(),
16            AuthorityType::Manager =>
17                StakePoolError::InvalidManagerAuthority.into(),
18            AuthorityType::Validator =>
19                StakePoolError::InvalidValidatorAuthority.into(),
20        });
21    }
22
23    Ok(())
24}
25
26pub fn process_update_validator_list(
27    program_id: &Pubkey,
28    accounts: &[AccountInfo],
29    validator: &Pubkey,
30) -> ProgramResult {
31    let account_info_iter = &mut accounts.iter();
32    
33    let stake_pool = next_account_info(account_info_iter)?;
34    let manager = next_account_info(account_info_iter)?;
35    
36    // Verify manager authority
37    verify_authority(
38        manager,
39        &stake_pool.manager,
40        AuthorityType::Manager,
41    )?;
42    
43    // Process update
44    // ...
45    
46    Ok(())
47}

2. Secure State Management

Implement secure state transitions and data validation.

State Validation

1#[derive(Debug)]
2pub enum StakePoolError {
3    // State validation errors
4    AlreadyInitialized,
5    InvalidState,
6    InvalidStakeAmount,
7    InsufficientFunds,
8    CalculationFailure,
9    
10    // Authority errors
11    SignatureMissing,
12    InvalidStakePoolAuthority,
13    InvalidManagerAuthority,
14    InvalidValidatorAuthority,
15    
16    // Operational errors
17    ValidatorListFull,
18    ValidatorNotFound,
19    DepositTooSmall,
20    WithdrawalTooLarge,
21}
22
23impl From<StakePoolError> for ProgramError {
24    fn from(e: StakePoolError) -> Self {
25        ProgramError::Custom(e as u32)
26    }
27}
28
29pub fn validate_stake_pool_state(
30    stake_pool: &StakePool,
31    validator_list: &ValidatorList,
32) -> ProgramResult {
33    // Verify stake pool is initialized
34    if !stake_pool.is_initialized {
35        return Err(StakePoolError::InvalidState.into());
36    }
37    
38    // Verify total stake matches validator stakes
39    let total_validator_stake = validator_list.validators
40        .iter()
41        .map(|v| v.active_stake_lamports)
42        .sum::<u64>();
43        
44    if total_validator_stake != stake_pool.total_lamports {
45        return Err(StakePoolError::InvalidState.into());
46    }
47    
48    // Verify pool token supply matches stake
49    if stake_pool.pool_token_supply == 0 && stake_pool.total_lamports > 0 {
50        return Err(StakePoolError::InvalidState.into());
51    }
52    
53    Ok(())
54}

3. Emergency Procedures

Implement emergency shutdown and fund recovery mechanisms.

Emergency Procedures

1pub fn process_emergency_pause(
2    program_id: &Pubkey,
3    accounts: &[AccountInfo],
4) -> ProgramResult {
5    let account_info_iter = &mut accounts.iter();
6    
7    let stake_pool = next_account_info(account_info_iter)?;
8    let manager = next_account_info(account_info_iter)?;
9    
10    // Verify manager authority
11    verify_authority(
12        manager,
13        &stake_pool.manager,
14        AuthorityType::Manager,
15    )?;
16    
17    // Pause deposits and withdrawals
18    stake_pool.state = PoolState::Paused;
19    
20    // Emit pause event
21    emit!(StakePoolPaused {
22        stake_pool: *stake_pool.key,
23        timestamp: Clock::get()?.unix_timestamp,
24    });
25    
26    Ok(())
27}
28
29pub fn process_emergency_unstake(
30    program_id: &Pubkey,
31    accounts: &[AccountInfo],
32) -> ProgramResult {
33    // Emergency unstake implementation
34    // ...
35    Ok(())
36}
Critical Security Measures
Always implement rate limiting, proper error handling, and secure RPC endpoints. Regular security audits and bug bounty programs are essential for protocol safety.

Performance Considerations

Optimizing performance is crucial for a liquid staking protocol to handle large volumes of stakes and ensure efficient operation. Here are key considerations and implementation patterns.

1. Compute Budget Optimization

Optimize instruction compute budget usage and implement batching where necessary.

Compute Optimization

1use solana_program::compute_budget::ComputeBudgetInstruction;
2
3pub fn optimize_compute_budget(
4    instructions: &mut Vec<Instruction>,
5    units: u32,
6) {
7    instructions.insert(
8        0,
9        ComputeBudgetInstruction::set_compute_unit_limit(units),
10    );
11}
12
13pub fn process_large_validator_list(
14    program_id: &Pubkey,
15    accounts: &[AccountInfo],
16    validators: &[Pubkey],
17) -> ProgramResult {
18    // Process validators in batches to stay within compute budget
19    for validator_batch in validators.chunks(10) {
20        process_validator_batch(program_id, accounts, validator_batch)?;
21    }
22    Ok(())
23}
24
25// Client-side batching
26const batchValidatorUpdates = async (
27    connection: Connection,
28    stakePool: PublicKey,
29    validators: PublicKey[],
30): Promise<void> => {
31    // Process in batches of 10
32    for (let i = 0; i < validators.length; i += 10) {
33        const batch = validators.slice(i, i + 10);
34        const tx = new Transaction();
35        
36        // Add compute budget instruction
37        tx.add(ComputeBudgetInstruction.setComputeUnitLimit({
38            units: 200_000,
39        }));
40        
41        // Add validator update instructions
42        batch.forEach(validator => {
43            tx.add(createValidatorUpdateInstruction(
44                stakePool,
45                validator,
46            ));
47        });
48        
49        await connection.sendTransaction(tx, [/* signers */]);
50    }
51}

2. State Compression

Implement efficient state storage and compression techniques.

State Compression

1#[derive(Clone, Debug, Default, PartialEq)]
2pub struct CompressedValidatorStakeInfo {
3    // Pack data efficiently
4    // 1 byte for status
5    pub status: u8,
6    // 4 bytes for vote account index
7    pub vote_account_index: u32,
8    // 8 bytes for stake
9    pub stake_lamports: u64,
10    // 2 bytes for commission (0-100)
11    pub commission: u16,
12}
13
14impl CompressedValidatorStakeInfo {
15    pub fn pack(&self, dst: &mut [u8]) -> Result<(), ProgramError> {
16        if dst.len() != 15 {
17            return Err(ProgramError::InvalidAccountData);
18        }
19        
20        dst[0] = self.status;
21        dst[1..5].copy_from_slice(&self.vote_account_index.to_le_bytes());
22        dst[5..13].copy_from_slice(&self.stake_lamports.to_le_bytes());
23        dst[13..15].copy_from_slice(&self.commission.to_le_bytes());
24        
25        Ok(())
26    }
27    
28    pub fn unpack(src: &[u8]) -> Result<Self, ProgramError> {
29        if src.len() != 15 {
30            return Err(ProgramError::InvalidAccountData);
31        }
32        
33        Ok(Self {
34            status: src[0],
35            vote_account_index: u32::from_le_bytes(src[1..5].try_into().unwrap()),
36            stake_lamports: u64::from_le_bytes(src[5..13].try_into().unwrap()),
37            commission: u16::from_le_bytes(src[13..15].try_into().unwrap()),
38        })
39    }
40}

3. Transaction Optimization

Optimize transaction structure and account loading.

Transaction Optimization

1interface TransactionBatch {
2  instructions: TransactionInstruction[];
3  requiredSigners: Signer[];
4}
5
6async function createOptimizedTransactions(
7  connection: Connection,
8  instructions: TransactionInstruction[],
9): Promise<TransactionBatch[]> {
10  const batches: TransactionBatch[] = [];
11  let currentBatch: TransactionBatch = {
12    instructions: [],
13    requiredSigners: [],
14  };
15  
16  // Track unique accounts to optimize lookups
17  const accountLookupTables = new Map<string, PublicKey>();
18  
19  for (const ix of instructions) {
20    // Check if adding instruction would exceed limits
21    if (currentBatch.instructions.length >= 10) {
22      batches.push(currentBatch);
23      currentBatch = {
24        instructions: [],
25        requiredSigners: [],
26      };
27    }
28    
29    // Add account lookup tables if needed
30    ix.keys.forEach(key => {
31      if (!accountLookupTables.has(key.pubkey.toBase58())) {
32        // Create lookup table for frequently used accounts
33        // ...
34      }
35    });
36    
37    currentBatch.instructions.push(ix);
38  }
39  
40  if (currentBatch.instructions.length > 0) {
41    batches.push(currentBatch);
42  }
43  
44  return batches;
45}
Performance Tips
Monitor your protocol's performance metrics and implement optimizations incrementally. Use proper benchmarking to validate performance improvements.

Implementation Checklist

  • 1

    Smart Contract Development

    Implement core program logic, state management, and instruction handlers

    Program Entry Point

    1use solana_program::{
    2    account_info::AccountInfo,
    3    entrypoint,
    4    entrypoint::ProgramResult,
    5    pubkey::Pubkey,
    6};
    7
    8entrypoint!(process_instruction);
    9
    10pub fn process_instruction(
    11    program_id: &Pubkey,
    12    accounts: &[AccountInfo],
    13    instruction_data: &[u8],
    14) -> ProgramResult {
    15    // Program logic implementation
    16    Ok(())
    17}
  • 2

    State Management

    Define protocol state structures and implement state transitions

  • 3

    Testing & Validation

    Implement comprehensive test suite and security validations

  • 4

    Client SDK Development

    Create TypeScript SDK for protocol interaction

Next Steps