3 Most Common Vulnerabilities in Solana Smart Contracts

We’re ByteScan.net, a team of security researchers who have spent the past ~24 months inspecting the internals of the Solana blockchain.
Solana is such a rapidly expanding ecosystem, and throughout our research, we found and reported several vulnerabilities in various Solana-based DeFi projects, eventually helping to secure the projects against attackers.

Since 2020, we’ve been working with developers from a range of projects building on Solana to assist them in securing their contracts. We’ve audited dozens of contracts, using our unique experience with Solana to uncover numerous exploitable bugs.

This article aims to raise attention around the three most common vulnerabilities in Solana contracts that we keep finding during our audits.
We’ll keep the vulnerability descriptions short and concise and provide a simplified example as well. We hope developers and other auditors will be able to use it. If you like to learn more about the ByteScan.net team, please check out https://bytescan.net.

1- Missing Ownership Control

Your contract must only trust accounts owned by itself. That means, as a Solana developer, you must always review the AccountInfo::owner field of accounts in your code. Note that these are not supposed to be wholly user-controlled. Therefore, you may create a helper function that takes an untrusted AccountInfo, inspects the owner, and returns an object of a different, trusted type.

A vulnerable example:

Consider the following code representing a vulnerable function called “withdraw_token_restricted”. The developer intended that this is an admin-only instruction to withdraw funds from the contract vault:

fn withdraw_funds(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let account_iter = &mut accounts.iter();
let vault = next_account_info(account_iter)?;
let admin = next_account_info(account_iter)?;
let config = ConfigAccount::unpack(next_account_info(account_iter)?)?;
let vault_authority = next_account_info(account_iter)?;


if config.admin != admin.pubkey() {
return Err(ProgramError::InvalidAdminAccount);
}

// …
// Transfer funds from vault to admin using vault_authority
// …

Ok(())
}

Hacking Scenario:

The function uses an account called config, which is supposed to include entrusted data to hold the admin pubkey and thus guarantee that only the admin account can use this instruction. Because the smart contract does not control that the valid entity owns the config, a hacker can provide a maliciously composed config account with a random admin field. Therefore, when the contract tries to verify that the given admin account is the admin account stored in its config account, it will be tricked by the hacker config. As a result, the hacker can easily steal the contract funds to her own account.

2- Missing Signer Check

If an instruction should only be open to a fixed set of entities, you must control that the right entity has signed the call by inspecting the AccountInfo::is_signer field.

Note that virtually any smart contract has instructions that are limited to be only called by specific entities, for example, admin-only instructions like locking the contract or user-specific instructions that alter the state of a user’s account. Although it seems pretty obvious to verify that the respective entity has signed the related transaction, these checks are usually forgotten.

A vulnerable example:

fn admin_update(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let config = ConfigAccount::unpack(next_account_info(account_iter)?)?;
let admin = next_account_info(account_iter)?;
let admin_new = next_account_info(account_iter)?;

// ...
// Validate the config account...
// ...

if admin.pubkey() != config.admin {
return Err(ProgramError::InvalidAdminAccount);
}

config.admin = admin_new.pubkey();

Ok(())
}

Hacking Scenario:

This function updates the contract admin. It tries to ensure that the instruction is only invokable by the current admin. And it controls it by comparing the admin account to the one in the current config account.

However, the code does not check whether the current admin has signed this operation. Therefore, an attacker can provide random accounts when invoking an instruction, so nothing prevents hackers from just providing the current admin as admin and their own account as new admin!
The instruction will replace the current admin with the new, malicious one, potentially giving the hacker full control over the contract.

3- Arithmetic underflow & overflow

In smart contracts, overflow/underflows are quite common because blockchain applications often compute math over financial data.

Both Solana smart contracts and Solana’s core runtime are written in Rust, and there have been several public reports about arithmetic overflows/underflows in Solana core runtime.

It may be a misconception that Rust is memory-safe, so it is free of arithmetic overflow/underflows for many developers, but this is only true in debug mode. However, when developers compile their contracts in release mode with the — release flag, Rust does not check for integer overflow that causes panics. Rather, if overflow occurs, Rust performs two’s complement wrapping. In brief, values greater than the maximum value the type can hold “wrap around” to the minimum of the type’s values. The program won’t panic, whereas the variable will have a value that probably isn’t what you expected.

Note that you’re compiling your contracts in release mode by using the Solana BPF toolchain ($ cargo build-bpf).

A vulnerable example:

let X: u32 = 1000; 

fn token_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u32) -> ProgramResult {

// ...
// deserialize & validate user and vault accounts
// ...

if amount + X > vault.user_balance[user_id] {
return Err(ProgramError::AttemptToWithdrawTooMuch);
}

// ...
// Transfer `amount` many tokens from vault to user-controlled account ...
// ...

Ok(())
}

The above code tries to guarantee a user can’t withdraw more than their earlier deposited balance, minus a fee from the vault.
Now imagine an attacker deposits 100,000 tokens. This will set vault.user_balance[user_id] to 100,000. Now, the hacker calls the token_withdraw function and sets the amount to u32::MAX-100 (i.e., 41,294,967,195). The arithmetic extra amount + X will wrap to 899. That is less than 100,000, and therefore, the hacker can pass the control, and the instruction will withdraw that amount of tokens from the vault, which is way more than she originally deposited.

This concludes our list of the top 3 common vulnerabilities in Solana programs. These are among the first things we check when we audit a contract. We’ve seen these vulnerabilities numerous times, and they often led to exploits that could completely drain vulnerable contracts.

However, note that checking for these bugs doesn’t constitute a full audit. We have not listed many other classes of vulnerabilities here, many of which stem from more sophisticated structures within a contract and hence cannot be identified through simple checks.

For more information about smart contract security audit, please visit https://bytescan.net

Author: Dr. Mo Ashouri (Co-Founder of ByteScan.Net)

Contact: mo@bytescan.net

Telegram: @mokoder

ByteScan.Net —

Blockchain Security Audit Team

Website: https://bytescan.net/

Email: hello@bytescan.net

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
bytescan

ByteScan is a Cyber Security Consulting Firm that offers security auditing services for a number of blockchains, with a special focus on third-generation dapps.