Solana Program Security – Part1

Solana is a web-scale, open-source blockchain protocol that is fast, secure, and fully decentralized. The protocol introduces eight core technologies that provide the infrastructure necessary for DApps and decentralized marketplaces. Solana uses a combination of proof-of-stake (PoS) and proof of history (PoH) consensus mechanisms to improve throughput and scalability. Consequently, the network claims to support 50,000 transactions per second (TPS), making it the fastest blockchain in the world.

In this post, we will talk about Solana program security, especially some common security vulnerabilities in Solana programs. This blog assumes advanced knowledge of the Solana program library and some basic understanding of Rust.

Introduction to Solana Programing Model

Smart Contracts in Solana are written in Rust or C, and they are called Programs. Solana Programs can own accounts and modify the data of the accounts they own. Other than on-chain programs which are developed and deployed by a Solana programmer, there are several native programs, which are required to run validator nodes. One of the native programs is the System Program. This program can create new accounts, allocate account data, assign accounts to owning programs, transfer lamports from System Program owned accounts and pay transaction fees.

Program id: 11111111111111111111111111111111
Instructions: SystemInstruction

Solana accounts 

An account in Solana contains several fields such as owner, data, lamports, executable …  which are set by the system program. If the program needs to store state between transactions, it does so by using data field of the account. Here, we provide some examples for accounts. You can see that account_1 and account_2 are owned by a token program and a stake pool program, respectively. The data of each account is therefore specified and updated by the owner program. Here is more detail on Solana accounts https://docs.solana.com/developing/programming-model/accounts.

System_Program -> account_1 -> {owner = token_program_id,
                                lamports, 
                                executable,
                                rent_epoch,
                                data -> Account {owner, state, mint…}
                                } 
System_Program -> account_2 -> {owner  = stake_pool_program_id
                                lamports, 
                                executable,
                                rent_epoch, 
                                data -> Stake_pool {manager, state, staker…} 
                                }

Solana Program flow

An app interacts with a Solana cluster by sending it transactions with one or more instructions. The Solana runtime passes those instructions to programs deployed by app developers beforehand. These instructions will be executed and validated by Solana Validators.

What can go wrong?

It is important to note that the Solana transaction structure specifies a list of public keys and signatures for those keys and a sequential list of instructions that will operate over the states associated with the account keys. This helps optimize the throughput, but because of this, malicious users can input arbitrary accounts, and it is now the program’s job to protect its state and data from the malicious input accounts.

Writing a Solana program is pretty simple if you know Rust or C, and understand the Solana programming model. Here are some examples (https://docs.solana.com/developing/on-chain-programs/examples). However, writing a “SECURE” program on Solana is non-trivial. As we discussed above, the program can write anything to the data of accounts it owns. Therefore, it is program’s responsibility to validate and protect its input account data.

We want to discuss two types of validations that are important and can be exploited (that may lead to loss of funds) if the program does not validate inputs properly.

1, Account ownership validation

One of the most important validations is to check whether the owners of the input accounts are the expected owners. For example, Solana stake-pool program needs to perform the following checks for every input stake account in order to ensure that input stake accounts are owned by the Solana stake program as expected.

/// Check stake program address
fn check_stake_program(program_id: &Pubkey) -> Result<(), ProgramError> {
    if *program_id != stake_program::id() {
        msg!(
            "Expected stake program {}, received {}",
            stake_program::id(),
            program_id
        );
        Err(ProgramError::IncorrectProgramId)
    } else {
        Ok(())
    }
}

We note that without the check_stake_program, it is possible for a malicious user to pass in accounts which are owned by a malicious program. Similarly, this function checks if the input accounts are indeed owned by the Solana system program.

/// Check system program address
fn check_system_program(program_id: &Pubkey) -> Result<(), ProgramError> {
    if *program_id != system_program::id() {
        msg!(
            "Expected system program {}, received {}",
            system_program::id(),
            program_id
        );
        Err(ProgramError::IncorrectProgramId)
    } else {
        Ok(())
    }
}

2, Account state (data) validation

Missing account state (data) validation is probably one of the highest severity mistakes that developers can easily make. For example, data of a reserve associated to a particular lending market in Solana token-lending program is specified by this struct. These fields will be instantiated when the reserve is initialized.

pub struct Reserve {
/// Version of the struct
pub version: u8,
/// Last slot when supply and rates updated
pub last_update: LastUpdate,
/// Lending market address
pub lending_market: Pubkey,
/// Reserve liquidity
pub liquidity: ReserveLiquidity,
/// Reserve collateral
pub collateral: ReserveCollateral,
/// Reserve configuration values
pub config: ReserveConfig,
}

So whenever the reserve is updated or used by a lending market, the program has to ensure that the input lending market is the one that the reserve has instantiated with. Otherwise, a malicious lending market can access the reserve and may be able to drain all funds. This check was missed in Solend, and lead to a potential loss of 2 millions (https://docs.google.com/document/d/1-WoQwT1QrPEX-r4N-fDamRQ50LM8DsdsOyq1iTabS3Q/edit#). Fortunately, the Solend team was able to detect and stop the exploitation in time such that no funds were stolen.

if &reserve.lending_market != lending_market_info.key {
msg!("Reserve lending market does not match the lending market provided");
return Err(LendingError::InvalidAccountInput.into());
}

Another example in Solana stake-pool program, a stake-pool is specified by this struct and these fields will be instantiated with some account pub-keys when the pool is initialized.

pub struct StakePool {
 
    pub account_type: AccountType,
    pub manager: Pubkey,
    pub staker: Pubkey,
    pub deposit_authority: Pubkey,
    pub withdraw_bump_seed: u8,
    pub validator_list: Pubkey,
    /// Reserve stake account, holds deactivated stake
    pub reserve_stake: Pubkey,

    ...

After the initialization, processes that involve the reserve stake account such as process_increase_validator_stake, process_update_validator_list_balance, process_update_validator_list_balance, and process_deposit have to check if the input reserve account is the same as the reserve_stake of the stake-pool.

pub fn check_reserve_stake(
        &self,
        reserve_stake_info: &AccountInfo,
    ) -> Result<(), ProgramError> {
        if *reserve_stake_info.key != self.reserve_stake {
            msg!(
                "Invalid reserve stake provided, expected {}, received {}",
                self.reserve_stake,
                reserve_stake_info.key
            );
            Err(StakePoolError::InvalidProgramAddress.into())
        } else {
            Ok(())
        }
    }

Conclusion

These two types of mistakes are very common, easy to make, and can potentially lead to loss of funds. Therefore, it is necessary to ensure that account owners and account states are validated before deploying the programs to the Solana main chain. In future blog posts, we will discuss some more advanced concepts in Solana as well as some other security vulnerabilities.

Leave a Reply