In the current blockchain ecosystem, Solana is known for its high performance and scalability. As a rapidly growing blockchain platform, Solana solves many challenges faced by traditional blockchains, such as transaction speed and scalability issues, through its unique architecture and innovative consensus mechanism. Solana’s high throughput and low latency make it ideal for decentralized finance (DeFi), NFT markets, and many other fields.
With its excellent security and performance advantages, the Rust language has become the language of choice for Solana blockchain smart contract development. Rust’s memory safety features and zero-cost abstractions give it significant advantages when developing efficient and secure blockchain applications. The strict compiler checks and concurrency provided by Rust enable developers to write efficient and safe code that avoids many common programming mistakes.
In this article, we will explore in detail the common security issues of developing smart contracts in the Solana blockchain using the Rust language. Through analysis of various vulnerabilities and security suggestions, we help developers improve the security of Solana smart contracts and ensure their reliability in decentralized applications.
Integer overflow and underflow occur when trying to store a value that exceeds the maximum or minimum value of the data type. Overflow occurs if the sum of a and b exceeds the maximum value of u32 (4,294,967,295). This can lead to unexpected behavior and potential security vulnerabilities.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if instruction_data.len() < 8 {
return Err(ProgramError::InvalidInstructionData);
}
let a = u32::from_le_bytes(instruction_data[0..4].try_into().unwrap());
let b = u32::from_le_bytes(instruction_data[4..8].try_into().unwrap());
let result = add(a, b);
msg!("Result of addition: {}", result);
Ok(())
}
// Vulnerability example: unchecked integer overflow
fn add(a: u32, b: u32) -> u32 {
a + b // If the sum of a and b exceeds the maximum value of u32, overflow will occur
}
Use Rust built-in checking functions to avoid integer overflows. For example, use the checked_add method.
fn add(a: u32, b: u32) -> Result<u32, &'static str> {
a.checked_add(b).ok_or("Integer overflow detected")
}
Rounding errors can result in loss of precision, especially in financial calculations where decimal calculations need to be handled precisely. For example, multiplying $12.345 by 100 and then rounding to two decimal places will result in inaccurate results.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if instruction_data.len() < 12 {
return Err(ProgramError::InvalidInstructionData);
}
let price = f64::from_bits(u64::from_le_bytes(instruction_data[0..8].try_into().unwrap()));
let quantity = u32::from_le_bytes(instruction_data[8..12].try_into().unwrap());
let total = calculate_total(price, quantity);
msg!("Total amount: {}", total);
Ok(())
}
//Example of vulnerability: rounding error
fn calculate_total(price: f64, quantity: u32) -> f64 {
let total = price * quantity as f64;
(total * 100.0).round() / 100.0 // Rounding to two decimal places, which may result in loss of accuracy
}
// Safe example: using fixed-point arithmetic
fn calculate_total(price: u64, quantity: u32) -> Result<u64, &'static str> {
// Define a fixed-point ratio, for example, 1e2 means keeping two decimal places
let scale = 100u64;
let scaled_price = price.checked_mul(scale).ok_or("Multiplication overflow")?;
let total = scaled_price.checked_mul(quantity as u64).ok_or("Multiplication overflow")?;
Ok(total)
}
Recursive calls may cause stack overflow.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if instruction_data.len() < 4 {
return Err(ProgramError::InvalidInstructionData);
}
let n = u32::from_le_bytes(instruction_data[0..4].try_into().unwrap());
recursive_function(n);
Ok(())
}
// Vulnerability example: recursive call, may cause stack overflow
fn recursive_function(n: u32) {
msg!("Recursion depth: {}", n);
if n > 0 {
recursive_function(n - 1); // Unlimited recursion, may cause stack overflow
}
}
Limit recursion depth or use an iterative approach.
//Safe example: use iteration method to avoid stack overflow
fn iterative_function(n: u32) {
let mut i = n;
while i > 0 {
msg!("Iteration step: {}", i);
i -= 1;
}
}
Attempts to allocate large amounts of memory, may cause OOM
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if instruction_data.len() < 8 {
return Err(ProgramError::InvalidInstructionData);
}
let size = usize::from_le_bytes(instruction_data[0..8].try_into().unwrap());
allocate_large_vector(size);
Ok(())
}
// Vulnerability example: Try to allocate a large amount of memory, which may cause OOM
fn allocate_large_vector(size: usize) {
let _large_vec = vec![0u8; size]; // The size is not checked, which may cause OOM
}
Check memory allocations and limit allocation sizes.
// Safe example: Check memory allocation and limit allocation size
fn allocate_large_vector(size: usize) -> Result<Vec<u8>, &'static str> {
const MAX_SIZE: usize = 1024 * 1024; // 1 MB limit
if size > MAX_SIZE {
return Err("Requested size exceeds limit");
}
Ok(vec![0u8; size])
}
In the code below, the function parameters are not strictly verified, which may lead to array out-of-bounds or other logic errors.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if instruction_data.is_empty() {
return Err(ProgramError::InvalidInstructionData);
}
let operation = instruction_data[0];
match operation {
0 => {
//Example operation, processing data
let result = unsafe_process_data(instruction_data);
if let Err(e) = result {
msg!("Error processing data: {}", e);
return Err(ProgramError::InvalidInstructionData);
}
Ok(())
}
_ => Err(ProgramError::InvalidInstructionData),
}
}
// Vulnerability example: Failure to verify whether the index is within the range may lead to out-of-bounds access
fn unsafe_process_data(data: &[u8]) -> Result<u8, &'static str> {
if data.len() < 9 {
return Err("Insufficient data length");
}
// Get the index and perform data processing
let index = usize::from_le_bytes(data[1..9].try_into().unwrap());
let value = data[index]; // Does not check whether the index is within the range, which may lead to out-of-bounds access
msg!("Processing data at index {}: {}", index, value);
Ok(value)
}
Strictly validate all function parameters to ensure they are within expected ranges.
In the code below, the contract allows any user to perform multiple initialization operations.
struct Contract {
owner: Option<Address>,
}
impl Contract {
pub fn new() -> Self {
Self { owner: None }
}
pub fn initialize(&mut self, owner: Address) {
self.owner = Some(owner);
}
}
Add an initialization check to ensure that initialization can only be performed once and only if it is not initialized yet.
struct Contract {
owner: Option<Address>,
is_initialized: bool,
}
impl Contract {
pub fn new() -> Self {
Self { owner: None, is_initialized: false }
}
pub fn initialize(&mut self, owner: Address) -> Result<(), &'static str> {
if self.is_initialized {
return Err("Already initialized");
}
self.owner = Some(owner);
self.is_initialized = true;
Ok(())
}
}
In the code below, the NFT owner is not verified as a transaction signer, allowing an attacker to cancel all sell orders.
struct NftContract {
owner: Address,
}
impl NftContract {
pub fn cancel_order(&mut self, nft_id: u64) {
// Assume orders is a hash map that stores all orders
self.orders.remove(&nft_id);
}
}
Verify that the NFT owner is the transaction signer before processing the order.
struct NftContract {
owner: Address,
}
impl NftContract {
pub fn cancel_order(&mut self, nft_id: u64, caller: Address) -> Result<(), &'static str> {
if self.owner_of(nft_id) != caller {
return Err("Caller is not the owner of the NFT");
}
self.orders.remove(&nft_id);
Ok(())
}
fn owner_of(&self, nft_id: u64) -> Address {
// Return the owner address corresponding to nft_id
}
}
In the code below, the submitted information is not strictly verified, and malicious users can submit false information to cause the program to crash.
struct Platform {
data: Vec<String>,
}
impl Platform {
pub fn submit_info(&mut self, info: String) {
self.data.push(info);
}
}
Submitted information is rigorously verified to ensure it conforms to expected format and content.
struct Platform {
data: Vec<String>,
}
impl Platform {
pub fn submit_info(&mut self, info: String) -> Result<(), &'static str> {
if !self.validate_info(&info) {
return Err("Invalid information submitted");
}
self.data.push(info);
Ok(())
}
fn validate_info(&self, info: &str) -> bool {
// Implement information verification logic, such as checking length, format, etc.
info.len() > 0 && info.len() <= 100 // For example, the message length must be between 1 and 100
}
}
In the code below, the transaction is not properly protected, allowing an attacker to pre-empt the transaction by increasing the fee.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
program_pack::{IsInitialized, Pack, Sealed},
program::{invoke, invoke_signed},
sysvar::{net::Rent, Sysvar},
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Order {
pub user: Pubkey,
pub amount: u64,
pub price: u64,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Dex {
pub orders: Vec<Order>,
}
impl Dex {
pub fn place_order(&mut self, order: Order) {
self.orders.push(order);
}
}
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Deserialize instruction data to get order details
let order: Order = Order::unpack_from_slice(instruction_data)?;
let mut dex = Dex::default();
// Place the order
dex.place_order(order);
msg!("Order placed: {:?}", dex.orders);
Ok(())
}
In the code below, the contract calls an unverified external contract, which may lead to unexpected security issues.
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
instruction::Instruction,
program::invoke,
pubkey::Pubkey,
program_error::ProgramError,
};
fn call_external_contract(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
invoke(
&Instruction {
program_id: *program_id,
accounts: accounts.to_vec(),
data: instruction_data.to_vec(),
},
accounts,
)
}
//Contract call example
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ExampleContract;
impl ExampleContract {
pub fn process(&self, program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
// Call external contract
call_external_contract(program_id, accounts, instruction_data)
}
}
Before calling an external contract, verify its origin and security to ensure it behaves as expected.
In the code below, different versions of dependencies may cause security risks. For example, a critical security vulnerability exists in a certain version of a dependency.
# Cargo.toml
[package]
name = "example_contract"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0.130" # A specific version of the dependency is used here
Try to use the latest version of your dependencies and specify the version range in Cargo.toml. Additionally, tools are available to check the security of dependencies.
# Cargo.toml
[package]
name = "example_contract"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = "1.0" # Use the version range to indicate compatibility with the latest version of the `1.0.x` series
use solana_program::pubkey::Pubkey;
struct Contract {
admin: Pubkey,
}
impl Contract {
pub fn restricted_action(&self, caller: &Pubkey) {
// No permission check is performed
self.perform_action();
}
fn perform_action(&self) {
//Perform certain restricted operations
println!("Performing a restricted action.");
}
}
fn main() {
let admin = Pubkey::new_unique();
let user = Pubkey::new_unique();
let contract = Contract { admin };
// The user attempts to invoke a restricted operation
contract.restricted_action(&user); // Unauthorized access
}
Perform permission checks before performing restricted operations.
use solana_program::pubkey::Pubkey;
use std::collections::HashMap;
struct Contract {
balances: HashMap<Pubkey, u64>,
}
impl Contract {
pub fn transfer(&mut self, from: Pubkey, to: Pubkey, amount: u64) {
// Did not check whether the account is the legal owner or whether the balance is sufficient
self.balances.insert(to, amount);
}
}
fn main() {
let mut contract = Contract {
balances: HashMap::new(),
};
let user1 = Pubkey::new_unique();
let user2 = Pubkey::new_unique();
contract.balances.insert(user1, 100);
//Illegal transfer, account balance and ownership not checked
contract.transfer(user1, user2, 50);
println!("User1 balance: {}", contract.balances.get(&user1).unwrap_or(&0));
println!("User2 balance: {}", contract.balances.get(&user2).unwrap_or(&0));
}
Verify the legitimacy of the account before transferring funds, ensuring that the account balance is sufficient and the account owner authorizes it.
use solana_program::pubkey::Pubkey;
use std::collections::HashMap;
struct Contract {
balances: HashMap<Pubkey, u64>,
}
impl Contract {
pub fn transfer(&mut self, from: Pubkey, to: Pubkey, amount: u64) -> Result<(), &'static str> {
// Check whether the account exists and whether the balance is sufficient
let from_balance = self.balances.get(&from).ok_or("Account not found")?;
if *from_balance < amount {
return Err("Insufficient balance");
}
//Perform transfer operation
*self.balances.get_mut(&from).unwrap() -= amount;
*self.balances.entry(to).or_insert(0) += amount;
Ok(())
}
}
fn main() {
let mut contract = Contract {
balances: HashMap::new(),
};
let user1 = Pubkey::new_unique();
let user2 = Pubkey::new_unique();
contract.balances.insert(user1, 100);
// Attempt a legal transfer
match contract.transfer(user1, user2, 50) {
Ok(_) => println!("Transfer successful."),
Err(e) => println!("Transfer failed: {}", e),
}
println!("User1 balance: {}", contract.balances.get(&user1).unwrap_or(&0));
println!("User2 balance: {}", contract.balances.get(&user2).unwrap_or(&0));
// Attempt to transfer illegally and exceed the balance
match contract.transfer(user1, user2, 100) {
Ok(_) => println!("Transfer successful."),
Err(e) => println!("Transfer failed: {}", e),
}
println!("User1 balance: {}", contract.balances.get(&user1).unwrap_or(&0));
println!("User2 balance: {}", contract.balances.get(&user2).unwrap_or(&0));
}
struct Contract {
balances: HashMap<Pubkey, u64>,
}
impl Contract {
pub fn deposit(&mut self, user: Pubkey, amount: u64) {
let balance = self.balances.entry(user).or_insert(0);
*balance += amount;
}
pub fn withdraw(&self, user: Pubkey, amount: u64) {
// Lack of withdrawal logic, resulting in funds being unable to be withdrawn
}
}
Make sure there is a complete fund transfer function in the contract and carry out appropriate permission checks.
impl Contract {
pub fn withdraw(&mut self, user: Pubkey, amount: u64) -> Result<(), &'static str> {
let balance = self.balances.get_mut(&user).ok_or("Account not found")?;
if *balance < amount {
return Err("Insufficient balance");
}
*balance -= amount;
// Transfer logic, transfer funds to user address
Ok(())
}
}
struct Contract {
total_supply: u64,
balances: HashMap<Pubkey, u64>,
}
impl Contract {
pub fn unstakes(&mut self, user: Pubkey, amount: u64) {
let balance = self.balances.get_mut(&user).unwrap();
*balance -= amount;
// The total supply is not updated, resulting in the token not being destroyed
}
}
Ensure the total supply is updated correctly when unstaking or burning tokens.
impl Contract {
pub fn unstakes(&mut self, user: Pubkey, amount: u64) -> Result<(), &'static str> {
let balance = self.balances.get_mut(&user).ok_or("Account not found")?;
if *balance < amount {
return Err("Insufficient balance");
}
*balance -= amount;
self.total_supply -= amount;
Ok(())
}
}
The following code example demonstrates a potential inconsistency between the documentation description and the code implementation:
Document description
/// Transfers `amount` tokens from the caller to `recipient`.
///
/// # Parameters
/// - `recipient`: The address of the recipient.
/// - `amount`: The number of tokens to transfer.
///
/// # Returns
/// - `Result<()>`: Returns an Ok result on success, or an Err result if an error occurs.
pub fn transfer(&self, recipient: Pubkey, amount: u64) -> Result<()> {
// Implementation
}
Actual code:
use solana_program::pubkey::Pubkey;
use std::collections::HashMap;
struct Contract {
balances: HashMap<Pubkey, u64>,
}
impl Contract {
/// Transfers `amount` tokens from `sender` to `recipient`.
///
/// # Parameters
/// - `sender`: The address of the sender.
/// - `recipient`: The address of the recipient.
/// - `amount`: The number of tokens to transfer.
///
/// # Returns
/// - `Result<()>`: Returns an Ok result on success, or an Err result if an error occurs.
pub fn transfer(&self, sender: Pubkey, recipient: Pubkey, amount: u64) -> Result<(), &'static str> {
let sender_balance = self.balances.get_mut(&sender).ok_or("Sender not found")?;
if *sender_balance < amount {
return Err("Insufficient balance");
}
*sender_balance -= amount;
let recipient_balance = self.balances.entry(recipient).or_insert(0);
*recipient_balance += amount;
Ok(())
}
}
fn main() {
let mut contract = Contract {
balances: HashMap::new(),
};
let user1 = Pubkey::new_unique();
let user2 = Pubkey::new_unique();
contract.balances.insert(user1, 100);
// Attempt a legal transfer
match contract.transfer(user1, user2, 50) {
Ok(_) => println!("Transfer successful."),
Err(e) => println!("Transfer failed: {}", e),
}
println!("User1 balance: {}", contract.balances.get(&user1).unwrap_or(&0));
println!("User2 balance: {}", contract.balances.get(&user2).unwrap_or(&0));
// Attempt to transfer illegally and exceed the balance
match contract.transfer(user1, user2, 100) {
Ok(_) => println!("Transfer successful."),
Err(e) => println!("Transfer failed: {}", e),
}
println!("User1 balance: {}", contract.balances.get(&user1).unwrap_or(&0));
println!("User2 balance: {}", contract.balances.get(&user2).unwrap_or(&0));
}
In this example, there is a potential inconsistency between the documentation description and the code implementation that could cause a developer or user to make an error when calling the method.
The following code shows an example of cross-contract call depth exceeding the limit:
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
program::invoke,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let target_program = next_account_info(account_info_iter)?;
// first call
invoke(
&create_instruction(program_id, target_program.key),
accounts,
)?;
// second call
invoke(
&create_instruction(program_id, target_program.key),
accounts,
)?;
// Third call
invoke(
&create_instruction(program_id, target_program.key),
accounts,
)?;
// fourth call
invoke(
&create_instruction(program_id, target_program.key),
accounts,
)?;
//The fifth call, depth limit exceeded
invoke(
&create_instruction(program_id, target_program.key),
accounts,
)?;
Ok(())
}
fn create_instruction(program_id: &Pubkey, target_program: &Pubkey) -> solana_program::instruction::Instruction {
solana_program::instruction::Instruction {
program_id: *program_id,
accounts: vec![],
data: thing![],
}
}
In this example, the fifth call to invoke would exceed Solana’s CPI depth limit, causing the program to fail.
In the Solana blockchain, the execution of smart contracts requires the consumption of Computation Units (CU). Currently, Solana places a cap on CU consumption for a single transaction, typically 48 million CUs. This is similar to the gas limit in Ethereum. Transactions exceeding this limit will fail, rendering the contract unexecutable. This restriction is mainly to prevent a single transaction from excessively consuming network resources and ensure the stability and efficiency of the network.
The following sample code shows an example of excessive compute unit consumption:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
program_error::ProgramError,
msg,
};
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let mut total = 0;
//Perform complex calculations, which may cause excessive CU consumption
msg!("Starting complex computation...");
for i in 0..100000000 {
total += i;
if i % 10000000 == 0 {
msg!("Processed {} iterations", i);
}
}
if total > 1000000000 {
return Err(ProgramError::InvalidInstructionData);
}
msg!("Complex computation completed with total: {}", total);
Ok(())
}
In this example, the loop iterates 100 million times. This complex calculation will cause the CU consumption to increase rapidly, which may exceed the 48 million CU limit, causing the transaction to fail.
In the code below, the user’s staking status is not checked, which may result in ineligible users claiming rewards. Users can claim rewards without any staking.
use std::collections::HashMap;
use solana_program::pubkey::Pubkey;
use solana_program::program_error::ProgramError;
struct StakingContract {
user_rewards: HashMap<Pubkey, u64>,
user_stakes: HashMap<Pubkey, u64>,
}
impl StakingContract {
pub fn claim_reward(&mut self, user: Pubkey) -> Result<(), ProgramError> {
let reward = self.user_rewards.get(&user).ok_or(ProgramError::InvalidArgument)?;
// Did not check whether the user is eligible to receive the reward
self.user_rewards.insert(user, 0);
Ok(())
}
}
Conduct necessary checks before claiming rewards to ensure that users are eligible to receive rewards.
use std::collections::HashMap;
use solana_program::pubkey::Pubkey;
use solana_program::program_error::ProgramError;
struct StakingContract {
user_rewards: HashMap<Pubkey, u64>,
user_stakes: HashMap<Pubkey, u64>,
}
impl StakingContract {
pub fn claim_reward(&mut self, user: Pubkey) -> Result<(), ProgramError> {
let reward = self.user_rewards.get(&user).ok_or(ProgramError::InvalidArgument)?;
// Check if the user has pledged
let stake = self.user_stakes.get(&user).ok_or(ProgramError::InvalidArgument)?;
if *stake == 0 {
return Err(ProgramError::InvalidArgument); // Without pledge, you cannot receive rewards
}
// Check if the reward is greater than zero
if *reward == 0 {
return Err(ProgramError::InvalidArgument); // There are no rewards to receive
}
//Execute the collection operation
self.user_rewards.insert(user, 0);
Ok(())
}
}
Here is a simple example of a hardcoded governance address:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
};
const GOVERNANCE_ADDRESS: &str = "HARD_CODED_GOVERNANCE_ADDRESS";
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let signer = next_account_info(account_info_iter)?;
if signer.key.to_string() != GOVERNANCE_ADDRESS {
return Err(ProgramError::IllegalOwner);
}
//Perform governance operations
Ok(())
}
In the above example, the governance address is hardcoded into the constant GOVERNANCE_ADDRESS, which means that the governance address cannot be changed dynamically.
A mechanism should be designed and implemented so that the governance address can be dynamically updated while ensuring that only the current governance address can perform this update operation.
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
program_error::ProgramError,
msg,
};
pub struct Governance {
governance_address: Pubkey,
}
impl Governance {
pub fn new(governance_address: Pubkey) -> Self {
Self { governance_address }
}
pub fn update_governance(&mut self, new_address: Pubkey, signer: &Pubkey) -> Result<(), ProgramError> {
if *signer != self.governance_address {
msg!("Only current governance address can update governance");
return Err(ProgramError::IllegalOwner);
}
self.governance_address = new_address;
Ok(())
}
}
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let signer = next_account_info(account_info_iter)?;
let mut governance = Governance::new(*signer.key);
// Assuming the instruction data contains the new governance address
let new_governance_address = Pubkey::new(&instruction_data[0..32]);
governance.update_governance(new_governance_address, signer.key)?;
Ok(())
}
In the optimized example, a Governance structure is implemented to manage governance addresses, and an update_governance method is provided to allow the current governance address to be updated to a new address. Only the current governance address can perform this update operation, ensuring security and resiliency.
CPI (Cross-Program Invocation) is a mechanism in Solana that allows one program to call the entry point of another program. Although this mechanism provides flexibility and functional extensibility for development, it also introduces new security risks. Special attention is required:
The following example code demonstrates a vulnerability where data is not properly validated in CPI calls:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::{invoke, invoke_signed},
pubkey::Pubkey,
sysvar::{net::Rent, Sysvar},
msg,
program_error::ProgramError,
};
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let source_account = next_account_info(account_info_iter)?;
let destination_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Build instructions to call other programs
let ix = solana_program::system_instruction::transfer(
source_account.key,
destination_account.key,
1000,
);
// Call other programs without validating data
invoke(
&ix,
&[source_account.clone(), destination_account.clone(), system_program.clone()],
)?;
Ok(())
}
In the above example, the transfer instruction of the system program is called, but the data is not verified.
When making cross-program calls, ensure the correct delivery and validation of data, and properly handle and validate returned responses.
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::{invoke, invoke_signed},
pubkey::Pubkey,
sysvar::{net::Rent, Sysvar},
msg,
program_error::ProgramError,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let source_account = next_account_info(account_info_iter)?;
let destination_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Verify whether the incoming account meets expectations
if !source_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
//Verify account data
if source_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Build instructions to call other programs
let ix = solana_program::system_instruction::transfer(
source_account.key,
destination_account.key,
1000,
);
// Call other programs and verify the returned results
invoke(
&ix,
&[source_account.clone(), destination_account.clone(), system_program.clone()],
)?;
Ok(())
}
In the optimized example, verification of accounts and data is added to ensure that the called data is correct and the returned results are processed correctly.
System calls (Syscalls) are the basis for interaction with the Solana runtime environment. Smart contracts usually require various system calls, such as obtaining account information, reading data, etc. These calls must be used and handled correctly to ensure the security and reliability of the contract.
The following example code demonstrates a vulnerability where errors are not properly handled when making system calls:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::invoke,
pubkey::Pubkey,
sysvar::{net::Rent, Sysvar},
};
entrypoint!(process_instruction);
fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let source_account = next_account_info(account_info_iter)?;
let destination_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Build system call instructions
let ix = solana_program::system_instruction::transfer(
source_account.key,
destination_account.key,
1000,
);
// Call the system program, but the error is not handled
invoke(
&ix,
&[source_account.clone(), destination_account.clone(), system_program.clone()],
)?;
Ok(())
}
In the above example, the error of the system call is not handled. If the call fails, the contract will not be able to catch and handle the error, which may lead to unexpected behavior.
Ensure proper error handling and validation when making system calls.
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::{invoke, invoke_signed},
pubkey::Pubkey,
sysvar::{net::Rent, Sysvar},
msg,
program_error::ProgramError,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let source_account = next_account_info(account_info_iter)?;
let destination_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Build system call instructions
let ix = solana_program::system_instruction::transfer(
source_account.key,
destination_account.key,
1000,
);
// Call system program and handle errors
match invoke(
&ix,
&[source_account.clone(), destination_account.clone(), system_program.clone()],
) {
Ok(_) => msg!("Transfer successful"),
Err(error) => {
msg!("Error during transfer: {:?}", error);
return Err(ProgramError::Custom(0)); // Custom error handling
}
}
Ok(())
}
In the optimized example, error handling for system calls is added to ensure that errors can be caught and handled when the call fails, thus improving the reliability and security of the contract.
PDA (Program Derived Addresses) is a special address in Solana, which is generated from the program seed and program ID to ensure uniqueness.
The following example code demonstrates potential problems with PDA generation without properly normalized seeds and multiple programs sharing the same PDA:
use solana_program::{
pubkey::Pubkey,
program_pack::Pack,
system_instruction,
sysvar::{net::Rent, Sysvar},
account_info::{AccountInfo, next_account_info},
entrypoint::ProgramResult,
program::invoke_signed,
};
fn create_pda(
program_id: &Pubkey,
accounts: &[AccountInfo],
seeds: &[&[u8]],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer_account = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let (pda, _bump_seed) = Pubkey::find_program_address(seeds, program_id);
// Seed not properly normalized
if *pda_account.key != pda {
return Err(ProgramError::InvalidSeeds);
}
// PDA sharing problem: multiple programs share the same PDA
// Suppose there is another program using the same seed to generate a PDA
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(pda_account.data_len());
let create_pda_instruction = system_instruction::create_account(
&payer_account.key,
&pda_account.key,
clean_lampports,
pda_account.data_len() as u64,
program_id,
);
invoke_signed(
&create_pda_instruction,
&[payer_account.clone(), pda_account.clone()],
&[&seeds],
)?;
Ok(())
}
In the example above, the PDA’s seed is not properly normalized, and there is a potential issue with multiple programs sharing the same PDA.
Ensure proper normalization of torrents and assess and avoid risks of PDA sharing.
use solana_program::{
pubkey::Pubkey,
program_pack::Pack,
system_instruction,
sysvar::{net::Rent, Sysvar},
account_info::{AccountInfo, next_account_info},
entrypoint::ProgramResult,
program::invoke_signed,
msg,
};
fn create_pda(
program_id: &Pubkey,
accounts: &[AccountInfo],
seeds: &[&[u8]],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer_account = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let (pda, bump_seed) = Pubkey::find_program_address(seeds, program_id);
// Correctly normalize seeds
if *pda_account.key != pda {
msg!("Invalid PDA seeds");
return Err(ProgramError::InvalidSeeds);
}
// Make sure the PDA is not shared by multiple programs
// If sharing is required, ensure access control and permission management for each program
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(pda_account.data_len());
let create_pda_instruction = system_instruction::create_account(
&payer_account.key,
&pda_account.key,
clean_lampports,
pda_account.data_len() as u64,
program_id,
);
invoke_signed(
&create_pda_instruction,
&[payer_account.clone(), pda_account.clone()],
&[&seeds],
)?;
Ok(())
}
In the optimized example, the correct standardization of seeds and the assessment of PDA sharing risks are added to ensure the safety of PDA generation and use.
Through this article, we take an in-depth look at common security issues when developing smart contracts using the Rust language on the Solana blockchain. We analyzed potential vulnerabilities in many aspects, including integer issues, memory and stack issues, parameter verification issues, initialization issues, DoS attacks, front-running issues, calling unknown code, etc., and put forward corresponding security suggestions.
ExVul is committed to providing customers with cutting-edge security solutions. As a professional blockchain security company, we have a team of senior blockchain security experts focusing on smart contract security audits, vulnerability detection and repair, and security consulting services. Through in-depth technical analysis and rigorous security testing, we help customers discover and solve potential security issues to ensure the security and stability of their blockchain applications.