From Ethereum Smart Contracts to Solana Programs: Two Common Security Pitfalls and Beyond

October 7, 2021

Ethereum is a well-know blockchain that supports “turing-complete” smart contracts. Solana is a rapidly growing blockchain that also supports smart contracts — called Solana programs, and it seems to be superior to Ethereum in terms of transactions per second and cost.

Why Solana programs are faster than Ethereum smart contracts? What are their key differences? This article explains an essential difference between the two and illustrates two common security pitfalls in Solana programs.

Code and data, couple or decouple?

In computer programs, there are always two things: (1) code — instructions that are executed by a computer’s CPU, GPU or other computing units; (2) data —inputs to the code or states of a program that are processed by the code. An essential difference between Ethereum and Solana lies in how code and data are represented in smart contracts.

In Ethereum, data and code are coupled together. A smart contract in Ethereum contains both the code and the data processed by the code. For example, in the Ownable contract written in Solidity below, the state variable _owner is data, and the function owner() is code. This coupled design is intuitive to write a smart contract, and it’s easy to understand the code. However, it makes Ethereum difficult to achieve high performance, since every smart contract has only a single copy of states and all transactions that touch the same state of a smart contract have to be executed sequentially. This is exactly the problem addressed by Solana.

contract Ownable is Context {
    address private _owner;
    function owner() public view virtual returns (address) {
        return _owner;        
    }
}

In Solana, data and code are decoupled. A Solana program contains only the code, and does not contain data. All data are provided as inputs to a Solana program. For example, in the Solana hello-world program written in Rust below, the function process_instruction is the code, and the state variable counter is the data of account, which is passed to the function as input.

pub fn process_instruction(
  program_id: &Pubkey,
  accounts: &[AccountInfo],
  _instruction_data: &[u8], 
) -> ProgramResult {
  let accounts_iter = &mut accounts.iter(); // Get the account to say hello to    
  let account = next_account_info(accounts_iter)?;  // Increment the counter    
  let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;    
  greeting_account.counter += 1;      
  greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
}

This decoupled design makes it possible to achieve high performance, since it allows executing many copies of a Solana program on different inputs in parallel. In other words, transactions from different user accounts to the same Solana program can run in parallel. This explains why Solana can achieve an amazingly 50000 TPS.

However, the downside is that it is more difficult to write Solana programs than Ethereum smart contracts, regardless of the language differences (e.g., Rust vs Solidity). In particular, Solana programs open new attack surfaces that do not exist in Ethereum smart contracts, because input accounts may not be trusted. We next illustrate two common security pitfalls in Solana programs: missing owner check and missing signer check.

Two common security pitfalls in Solana programs

In Solana, since all accounts are provided as inputs when invoking a Solana program, users can supply arbitrary accounts and there’s no built-in stopping a malicious user from doing so with fake data. Therefore, Solana programs must check the validity of the input accounts.

We use the function process_set_lending_market_owner from Solana program library to illustrate the importance of these checks.

A sample code from Solana program library

The function above is to update the owner of lending_market to new_owner . There are two types of checks: owner check and signer check. Missing any of them may leave the function vulnerable to attacks.

Owner Checker (line 178):

if &lending_market.owner != lending_market_owner_info.key

The owner check on line 178 ensures that the lending_market_owner_info account is the current owner of lending_market. If this check is missed, then a user who is not the owner may successfully change the owner of lending_market .

Signer Checker (line 182):

if !lending_market_owner_info.is_signer

The signer check on line 182 ensures that the lending_market_owner_info account (which has passed the owner check on line 178), is signer. Otherwise if this check is missed, then an attack can simply supply the valid lending_market_owner_info as input to the function (because the attack does not need to sign the account) and then set owner of lending_market to any new_owner chosen by the attacker.

Beyond the two missing checks

Besides these two checks, there are several other common pitfalls in Solana programs. A summary of them can be found in a recent blog by Neodyme. According to Neodyme, auditing these common pitfalls have prevented the loss of billion-dollar worth of assets.

Soteria is a new security tool that automatically scans common pitfalls in Solana programs.

For all blogs by Soteria, Please visit https://www.soteria.dev/blogs