Smart Contracts

Learn about smart contract development for liquid staking protocols.

Last updated: 2024-03-21
Edit on GitHub

Overview

The smart contract system is the foundation of your liquid staking protocol on Solana. This guide covers the core contracts, their interactions, security patterns, and best practices for building a robust and secure protocol.

Smart Contracts Architecture

Core Contracts

The protocol consists of several core smart contracts that work together to provide liquid staking functionality.

Stake Pool Program

1use solana_program::{
2    account_info::AccountInfo,
3    entrypoint,
4    entrypoint::ProgramResult,
5    program_error::ProgramError,
6    pubkey::Pubkey,
7    system_program,
8    sysvar::{clock::Clock, rent::Rent},
9};
10
11#[derive(Clone, Debug, PartialEq)]
12pub struct StakePool {
13    /// Protocol version for upgrade management
14    pub version: u8,
15    
16    /// Manager authority
17    pub manager: Pubkey,
18    
19    /// Staker authority
20    pub staker: Pubkey,
21    
22    /// Withdraw authority
23    pub withdraw_authority: Pubkey,
24    
25    /// Validator list storage account
26    pub validator_list: Pubkey,
27    
28    /// Reserve stake account
29    pub reserve_stake: Pubkey,
30    
31    /// Pool token mint
32    pub pool_mint: Pubkey,
33    
34    /// Manager fee account
35    pub manager_fee_account: Pubkey,
36    
37    /// Total pool stake in lamports
38    pub total_lamports: u64,
39    
40    /// Total supply of pool tokens
41    pub pool_token_supply: u64,
42    
43    /// Last epoch stake pool was updated
44    pub last_update_epoch: u64,
45    
46    /// Fee schedule
47    pub fee_schedule: FeeSchedule,
48    
49    /// Protocol state
50    pub state: ProtocolState,
51}
52
53#[derive(Clone, Debug, PartialEq)]
54pub enum ProtocolState {
55    Uninitialized,
56    Active,
57    Paused,
58    Emergency,
59}
60
61entrypoint!(process_instruction);
62
63pub fn process_instruction(
64    program_id: &Pubkey,
65    accounts: &[AccountInfo],
66    instruction_data: &[u8],
67) -> ProgramResult {
68    let instruction = StakePoolInstruction::try_from_slice(instruction_data)?;
69    
70    match instruction {
71        StakePoolInstruction::Initialize { fee_schedule } => {
72            process_initialize(program_id, accounts, fee_schedule)
73        }
74        StakePoolInstruction::AddValidator { validator } => {
75            process_add_validator(program_id, accounts, validator)
76        }
77        StakePoolInstruction::RemoveValidator { validator } => {
78            process_remove_validator(program_id, accounts, validator)
79        }
80        StakePoolInstruction::Deposit { lamports } => {
81            process_deposit(program_id, accounts, lamports)
82        }
83        StakePoolInstruction::Withdraw { pool_tokens } => {
84            process_withdraw(program_id, accounts, pool_tokens)
85        }
86        StakePoolInstruction::UpdatePoolBalance => {
87            process_update_pool_balance(program_id, accounts)
88        }
89        StakePoolInstruction::ClaimRewards => {
90            process_claim_rewards(program_id, accounts)
91        }
92        StakePoolInstruction::EmergencyPause => {
93            process_emergency_pause(program_id, accounts)
94        }
95    }
96}

Validator List Contract

1#[derive(Clone, Debug, 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, 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    /// Performance metrics
31    pub metrics: ValidatorMetrics,
32}
33
34impl ValidatorList {
35    pub fn add_validator(
36        &mut self,
37        vote_account: Pubkey,
38    ) -> ProgramResult {
39        if self.validators.len() >= self.max_validators as usize {
40            return Err(StakePoolError::MaxValidatorsReached.into());
41        }
42        
43        // Check if validator already exists
44        if self.validators.iter().any(|v| v.vote_account_address == vote_account) {
45            return Err(StakePoolError::ValidatorAlreadyExists.into());
46        }
47        
48        // Add new validator
49        self.validators.push(ValidatorStakeInfo {
50            vote_account_address: vote_account,
51            active_stake_lamports: 0,
52            transient_stake_lamports: 0,
53            last_update_epoch: Clock::get()?.epoch,
54            status: ValidatorStatus::Active,
55            metrics: ValidatorMetrics::default(),
56        });
57        
58        Ok(())
59    }
60    
61    pub fn remove_validator(
62        &mut self,
63        vote_account: &Pubkey,
64    ) -> ProgramResult {
65        let validator_index = self.validators
66            .iter()
67            .position(|v| v.vote_account_address == *vote_account)
68            .ok_or(StakePoolError::ValidatorNotFound)?;
69            
70        // Check if validator has no stake
71        let validator = &self.validators[validator_index];
72        if validator.active_stake_lamports > 0 || validator.transient_stake_lamports > 0 {
73            return Err(StakePoolError::ValidatorHasStake.into());
74        }
75        
76        // Remove validator
77        self.validators.remove(validator_index);
78        
79        Ok(())
80    }
81}

Token Management Contract

1use spl_token::{
2    instruction::{initialize_mint, mint_to, burn},
3    state::{Account, Mint},
4};
5
6pub struct TokenManager {
7    /// Pool token mint
8    pub pool_mint: Pubkey,
9    
10    /// Mint authority
11    pub mint_authority: Pubkey,
12    
13    /// Exchange rate
14    pub exchange_rate: ExchangeRate,
15}
16
17impl TokenManager {
18    pub fn initialize_mint(
19        &self,
20        program_id: &Pubkey,
21        accounts: &[AccountInfo],
22    ) -> ProgramResult {
23        let account_info_iter = &mut accounts.iter();
24        
25        let mint_account = next_account_info(account_info_iter)?;
26        let rent_account = next_account_info(account_info_iter)?;
27        let authority_account = next_account_info(account_info_iter)?;
28        
29        // Create mint account
30        let rent = &Rent::from_account_info(rent_account)?;
31        let required_lamports = rent.minimum_balance(Mint::LEN);
32        
33        if mint_account.lamports() < required_lamports {
34            let lamports_diff = required_lamports.saturating_sub(mint_account.lamports());
35            invoke(
36                &system_instruction::transfer(
37                    authority_account.key,
38                    mint_account.key,
39                    lamports_diff,
40                ),
41                &[authority_account.clone(), mint_account.clone()],
42            )?;
43        }
44        
45        // Initialize mint
46        invoke(
47            &initialize_mint(
48                &spl_token::id(),
49                mint_account.key,
50                authority_account.key,
51                Some(authority_account.key),
52                9, // 9 decimals to match SOL
53            )?,
54            &[mint_account.clone(), rent_account.clone()],
55        )?;
56        
57        Ok(())
58    }
59    
60    pub fn mint_tokens(
61        &self,
62        program_id: &Pubkey,
63        accounts: &[AccountInfo],
64        amount: u64,
65    ) -> ProgramResult {
66        let account_info_iter = &mut accounts.iter();
67        
68        let mint_account = next_account_info(account_info_iter)?;
69        let destination = next_account_info(account_info_iter)?;
70        let authority = next_account_info(account_info_iter)?;
71        
72        // Calculate token amount based on exchange rate
73        let token_amount = self.exchange_rate.sol_to_token(amount)?;
74        
75        // Mint tokens
76        invoke_signed(
77            &mint_to(
78                &spl_token::id(),
79                mint_account.key,
80                destination.key,
81                authority.key,
82                &[],
83                token_amount,
84            )?,
85            &[mint_account.clone(), destination.clone(), authority.clone()],
86            &[&[&program_id.to_bytes(), &[self.bump_seed]]],
87        )?;
88        
89        Ok(())
90    }
91    
92    pub fn burn_tokens(
93        &self,
94        program_id: &Pubkey,
95        accounts: &[AccountInfo],
96        amount: u64,
97    ) -> ProgramResult {
98        let account_info_iter = &mut accounts.iter();
99        
100        let source = next_account_info(account_info_iter)?;
101        let mint = next_account_info(account_info_iter)?;
102        let authority = next_account_info(account_info_iter)?;
103        
104        // Burn tokens
105        invoke_signed(
106            &burn(
107                &spl_token::id(),
108                source.key,
109                mint.key,
110                authority.key,
111                &[],
112                amount,
113            )?,
114            &[source.clone(), mint.clone(), authority.clone()],
115            &[&[&program_id.to_bytes(), &[self.bump_seed]]],
116        )?;
117        
118        Ok(())
119    }
120}

Security Patterns

Implement robust security patterns to protect user funds and ensure protocol safety.

Access Control Implementation

1pub struct AccessControl {
2    /// Manager authority
3    pub manager: Pubkey,
4    
5    /// Staker authority
6    pub staker: Pubkey,
7    
8    /// Emergency authority
9    pub emergency: Pubkey,
10    
11    /// Pending authority changes
12    pub pending_changes: Option<PendingAuthority>,
13}
14
15impl AccessControl {
16    pub fn verify_authority(
17        &self,
18        authority: &AccountInfo,
19        authority_type: AuthorityType,
20    ) -> ProgramResult {
21        // Verify signature
22        if !authority.is_signer {
23            return Err(StakePoolError::SignatureMissing.into());
24        }
25        
26        // Verify authority matches expected
27        match authority_type {
28            AuthorityType::Manager => {
29                if authority.key != &self.manager {
30                    return Err(StakePoolError::InvalidManagerAuthority.into());
31                }
32            }
33            AuthorityType::Staker => {
34                if authority.key != &self.staker {
35                    return Err(StakePoolError::InvalidStakerAuthority.into());
36                }
37            }
38            AuthorityType::Emergency => {
39                if authority.key != &self.emergency {
40                    return Err(StakePoolError::InvalidEmergencyAuthority.into());
41                }
42            }
43        }
44        
45        Ok(())
46    }
47    
48    pub fn change_authority(
49        &mut self,
50        current_authority: &AccountInfo,
51        new_authority: Pubkey,
52        authority_type: AuthorityType,
53    ) -> ProgramResult {
54        // Verify current authority
55        self.verify_authority(current_authority, authority_type.clone())?;
56        
57        // Set pending change
58        self.pending_changes = Some(PendingAuthority {
59            authority_type,
60            new_authority,
61            effective_epoch: Clock::get()?.epoch + AUTHORITY_CHANGE_DELAY,
62        });
63        
64        Ok(())
65    }
66}

Emergency Procedures

1pub struct EmergencyModule {
2    /// Emergency authority
3    pub emergency_authority: Pubkey,
4    
5    /// Protocol state
6    pub state: ProtocolState,
7    
8    /// Emergency timelock
9    pub timelock: Option<i64>,
10    
11    /// Required confirmations
12    pub required_confirmations: u8,
13}
14
15impl EmergencyModule {
16    pub fn initiate_emergency_shutdown(
17        &mut self,
18        authority: &AccountInfo,
19    ) -> ProgramResult {
20        // Verify emergency authority
21        if !authority.is_signer || authority.key != &self.emergency_authority {
22            return Err(StakePoolError::InvalidEmergencyAuthority.into());
23        }
24        
25        // Set emergency state
26        self.state = ProtocolState::Emergency;
27        
28        // Start timelock
29        self.timelock = Some(Clock::get()?.unix_timestamp);
30        
31        // Emit event
32        emit!(EmergencyShutdown {
33            authority: *authority.key,
34            timestamp: self.timelock.unwrap(),
35        });
36        
37        Ok(())
38    }
39    
40    pub fn process_emergency_unstake(
41        &mut self,
42        accounts: &[AccountInfo],
43    ) -> ProgramResult {
44        // Verify emergency state
45        if self.state != ProtocolState::Emergency {
46            return Err(StakePoolError::InvalidState.into());
47        }
48        
49        // Verify timelock has passed
50        let current_time = Clock::get()?.unix_timestamp;
51        if current_time < self.timelock.unwrap() + EMERGENCY_TIMELOCK_DURATION {
52            return Err(StakePoolError::TimelockNotExpired.into());
53        }
54        
55        // Process unstake
56        self.unstake_all(accounts)?;
57        
58        Ok(())
59    }
60}

State Management

Implement secure state management and transitions.

State Management Implementation

1#[derive(Clone, Debug, PartialEq)]
2pub enum ProtocolState {
3    Uninitialized,
4    Active,
5    Paused,
6    Emergency,
7}
8
9pub struct StateManager {
10    /// Current state
11    pub state: ProtocolState,
12    
13    /// State transition history
14    pub history: Vec<StateTransition>,
15    
16    /// State-specific parameters
17    pub parameters: StateParameters,
18}
19
20impl StateManager {
21    pub fn transition_state(
22        &mut self,
23        new_state: ProtocolState,
24        authority: &AccountInfo,
25    ) -> ProgramResult {
26        // Verify transition is valid
27        self.verify_state_transition(new_state.clone())?;
28        
29        // Verify authority
30        match new_state {
31            ProtocolState::Emergency => {
32                // Only emergency authority can trigger emergency state
33                if !authority.is_signer || authority.key != &self.parameters.emergency_authority {
34                    return Err(StakePoolError::InvalidEmergencyAuthority.into());
35                }
36            }
37            _ => {
38                // Manager authority required for other transitions
39                if !authority.is_signer || authority.key != &self.parameters.manager {
40                    return Err(StakePoolError::InvalidManagerAuthority.into());
41                }
42            }
43        }
44        
45        // Record transition
46        self.history.push(StateTransition {
47            from: self.state.clone(),
48            to: new_state.clone(),
49            authority: *authority.key,
50            timestamp: Clock::get()?.unix_timestamp,
51        });
52        
53        // Update state
54        self.state = new_state;
55        
56        Ok(())
57    }
58    
59    fn verify_state_transition(
60        &self,
61        new_state: ProtocolState,
62    ) -> ProgramResult {
63        match (self.state.clone(), new_state) {
64            // Allow transition from Uninitialized to Active
65            (ProtocolState::Uninitialized, ProtocolState::Active) => Ok(()),
66            
67            // Allow transition from Active to Paused
68            (ProtocolState::Active, ProtocolState::Paused) => Ok(()),
69            
70            // Allow transition from Paused to Active
71            (ProtocolState::Paused, ProtocolState::Active) => Ok(()),
72            
73            // Allow transition to Emergency from any state except Uninitialized
74            (ProtocolState::Uninitialized, ProtocolState::Emergency) => {
75                Err(StakePoolError::InvalidStateTransition.into())
76            }
77            (_, ProtocolState::Emergency) => Ok(()),
78            
79            // All other transitions are invalid
80            _ => Err(StakePoolError::InvalidStateTransition.into()),
81        }
82    }
83}

Testing Guidelines

Implement comprehensive tests to ensure protocol security and reliability.

Test Implementation

1#[cfg(test)]
2mod tests {
3    use super::*;
4    use solana_program_test::*;
5    use solana_sdk::signature::Signer;
6
7    #[tokio::test]
8    async fn test_stake_pool_initialization() {
9        // Create program test environment
10        let program_test = ProgramTest::new(
11            "my_stake_pool",
12            id(),
13            processor!(process_instruction),
14        );
15        
16        let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
17        
18        // Generate necessary keypairs
19        let stake_pool_keypair = Keypair::new();
20        let validator_list_keypair = Keypair::new();
21        let pool_mint_keypair = Keypair::new();
22        
23        // Create initialization instruction
24        let init_ix = initialize_stake_pool(
25            &id(),
26            &stake_pool_keypair.pubkey(),
27            &validator_list_keypair.pubkey(),
28            &pool_mint_keypair.pubkey(),
29            &payer.pubkey(),
30            &FeeSchedule::default(),
31        );
32        
33        // Create and sign transaction
34        let mut transaction = Transaction::new_with_payer(
35            &[init_ix],
36            Some(&payer.pubkey()),
37        );
38        transaction.sign(
39            &[&payer, &stake_pool_keypair],
40            recent_blockhash,
41        );
42        
43        // Submit and confirm transaction
44        banks_client
45            .process_transaction(transaction)
46            .await
47            .unwrap();
48            
49        // Verify initialization
50        let stake_pool = get_stake_pool(&mut banks_client, &stake_pool_keypair.pubkey())
51            .await
52            .unwrap();
53            
54        assert!(stake_pool.is_initialized);
55        assert_eq!(stake_pool.total_lamports, 0);
56    }
57
58    #[tokio::test]
59    async fn test_validator_management() {
60        let (mut banks_client, stake_pool, manager) = setup_stake_pool().await;
61        
62        // Add validator
63        let validator = Keypair::new();
64        let add_validator_ix = add_validator(
65            &id(),
66            &stake_pool.pubkey(),
67            &manager.pubkey(),
68            &validator.pubkey(),
69        );
70        
71        let mut transaction = Transaction::new_with_payer(
72            &[add_validator_ix],
73            Some(&manager.pubkey()),
74        );
75        transaction.sign(
76            &[&manager],
77            recent_blockhash,
78        );
79        
80        banks_client
81            .process_transaction(transaction)
82            .await
83            .unwrap();
84            
85        // Verify validator was added
86        let validator_list = get_validator_list(&mut banks_client, &stake_pool.validator_list)
87            .await
88            .unwrap();
89            
90        assert_eq!(validator_list.validators.len(), 1);
91        assert_eq!(
92            validator_list.validators[0].vote_account_address,
93            validator.pubkey(),
94        );
95    }
96
97    #[tokio::test]
98    async fn test_emergency_procedures() {
99        let (mut banks_client, stake_pool, emergency_authority) = setup_stake_pool().await;
100        
101        // Initiate emergency shutdown
102        let emergency_ix = emergency_shutdown(
103            &id(),
104            &stake_pool.pubkey(),
105            &emergency_authority.pubkey(),
106        );
107        
108        let mut transaction = Transaction::new_with_payer(
109            &[emergency_ix],
110            Some(&emergency_authority.pubkey()),
111        );
112        transaction.sign(
113            &[&emergency_authority],
114            recent_blockhash,
115        );
116        
117        banks_client
118            .process_transaction(transaction)
119            .await
120            .unwrap();
121            
122        // Verify emergency state
123        let stake_pool = get_stake_pool(&mut banks_client, &stake_pool.pubkey())
124            .await
125            .unwrap();
126            
127        assert_eq!(stake_pool.state, ProtocolState::Emergency);
128    }
129}
Security Considerations
  • Always implement proper access controls and authority verification
  • Use checked math operations to prevent overflows
  • Implement comprehensive error handling
  • Add proper event logging for all important operations
  • Include emergency procedures and safety mechanisms
  • Maintain proper state validation and transitions