Smart Contracts
Learn about smart contract development for liquid staking protocols.
Last updated: 2024-03-21
Edit on GitHubOverview
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.
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