1134 lines
28 KiB
Markdown
1134 lines
28 KiB
Markdown
# Native Rust Security Patterns for Solana Programs
|
|
|
|
This reference covers security vulnerabilities and best practices specific to Solana programs built with native Rust (without Anchor framework).
|
|
|
|
## Table of Contents
|
|
|
|
1. [Manual Account Validation](#manual-account-validation)
|
|
2. [Account Discriminator Patterns](#account-discriminator-patterns)
|
|
3. [PDA Security in Native Rust](#pda-security-in-native-rust)
|
|
4. [Manual CPI Security](#manual-cpi-security)
|
|
5. [Manual Serialization Security](#manual-serialization-security)
|
|
6. [Rent and Space Management](#rent-and-space-management)
|
|
7. [Error Handling in Native Rust](#error-handling-in-native-rust)
|
|
8. [Token Program Integration](#token-program-integration)
|
|
9. [Low-Level Security Patterns](#low-level-security-patterns)
|
|
10. [Native Rust Best Practices](#native-rust-best-practices)
|
|
|
|
---
|
|
|
|
## Manual Account Validation
|
|
|
|
In native Rust programs, ALL account validation must be performed manually. Missing any check can lead to critical vulnerabilities.
|
|
|
|
### Signer Checks
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn process_instruction(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
_instruction_data: &[u8],
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let authority = next_account_info(account_info_iter)?;
|
|
|
|
// Missing signer check - anyone can call this!
|
|
// Perform privileged operation
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn process_instruction(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
_instruction_data: &[u8],
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let authority = next_account_info(account_info_iter)?;
|
|
|
|
if !authority.is_signer {
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
|
|
// Now safe to perform privileged operation
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Owner Validation
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn update_config(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let config_account = next_account_info(account_info_iter)?;
|
|
|
|
// Missing owner check - could be any account!
|
|
let mut config_data = Config::try_from_slice(&config_account.data.borrow())?;
|
|
config_data.value = 42;
|
|
config_data.serialize(&mut *config_account.data.borrow_mut())?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn update_config(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let config_account = next_account_info(account_info_iter)?;
|
|
|
|
// Verify this account is owned by our program
|
|
if config_account.owner != program_id {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
|
|
let mut config_data = Config::try_from_slice(&config_account.data.borrow())?;
|
|
config_data.value = 42;
|
|
config_data.serialize(&mut *config_account.data.borrow_mut())?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Writable Checks
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn transfer_tokens(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let source = next_account_info(account_info_iter)?;
|
|
|
|
// Missing writable check - runtime will panic!
|
|
let mut data = source.try_borrow_mut_data()?;
|
|
// Modify data...
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn transfer_tokens(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let source = next_account_info(account_info_iter)?;
|
|
|
|
if !source.is_writable {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
let mut data = source.try_borrow_mut_data()?;
|
|
// Safe to modify data
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Comprehensive Validation Function
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
pub struct AccountValidation<'a, 'info> {
|
|
account: &'a AccountInfo<'info>,
|
|
}
|
|
|
|
impl<'a, 'info> AccountValidation<'a, 'info> {
|
|
pub fn new(account: &'a AccountInfo<'info>) -> Self {
|
|
Self { account }
|
|
}
|
|
|
|
pub fn owner(self, expected_owner: &Pubkey) -> Result<Self, ProgramError> {
|
|
if self.account.owner != expected_owner {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn signer(self) -> Result<Self, ProgramError> {
|
|
if !self.account.is_signer {
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn writable(self) -> Result<Self, ProgramError> {
|
|
if !self.account.is_writable {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn key(self, expected_key: &Pubkey) -> Result<Self, ProgramError> {
|
|
if self.account.key != expected_key {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn initialized(self) -> Result<Self, ProgramError> {
|
|
if self.account.data_is_empty() {
|
|
return Err(ProgramError::UninitializedAccount);
|
|
}
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
// Usage:
|
|
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let authority = next_account_info(account_info_iter)?;
|
|
let config = next_account_info(account_info_iter)?;
|
|
|
|
AccountValidation::new(authority)
|
|
.signer()?;
|
|
|
|
AccountValidation::new(config)
|
|
.owner(program_id)?
|
|
.writable()?
|
|
.initialized()?;
|
|
|
|
// All validations passed
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Account Discriminator Patterns
|
|
|
|
Without Anchor's automatic discriminators, you must manually implement account type safety.
|
|
|
|
### Why Discriminators Matter
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct ConfigAccount {
|
|
pub admin: Pubkey,
|
|
pub value: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct UserAccount {
|
|
pub owner: Pubkey,
|
|
pub balance: u64,
|
|
}
|
|
|
|
pub fn update_config(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let config = next_account_info(&mut accounts.iter())?;
|
|
|
|
// No discriminator check - UserAccount has same layout!
|
|
let mut data = ConfigAccount::try_from_slice(&config.data.borrow())?;
|
|
data.value = 999;
|
|
// Could be writing to a UserAccount!
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Implementing Discriminators
|
|
|
|
**Secure:**
|
|
```rust
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
|
|
|
pub const CONFIG_DISCRIMINATOR: u64 = 0x1234567890ABCDEF;
|
|
pub const USER_DISCRIMINATOR: u64 = 0xFEDCBA0987654321;
|
|
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct ConfigAccount {
|
|
pub discriminator: u64,
|
|
pub admin: Pubkey,
|
|
pub value: u64,
|
|
}
|
|
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct UserAccount {
|
|
pub discriminator: u64,
|
|
pub owner: Pubkey,
|
|
pub balance: u64,
|
|
}
|
|
|
|
impl ConfigAccount {
|
|
pub const LEN: usize = 8 + 32 + 8;
|
|
|
|
pub fn new(admin: Pubkey, value: u64) -> Self {
|
|
Self {
|
|
discriminator: CONFIG_DISCRIMINATOR,
|
|
admin,
|
|
value,
|
|
}
|
|
}
|
|
|
|
pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
|
|
let data = account.data.borrow();
|
|
if data.len() < 8 {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
let discriminator = u64::from_le_bytes(data[0..8].try_into().unwrap());
|
|
if discriminator != CONFIG_DISCRIMINATOR {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
Self::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData)
|
|
}
|
|
}
|
|
|
|
pub fn update_config(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
) -> ProgramResult {
|
|
let config_account = next_account_info(&mut accounts.iter())?;
|
|
|
|
// Discriminator validated during deserialization
|
|
let mut config = ConfigAccount::from_account_info(config_account)?;
|
|
config.value = 999;
|
|
config.serialize(&mut *config_account.data.borrow_mut())?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Alternative: String-Based Discriminators
|
|
|
|
```rust
|
|
pub const ACCOUNT_TYPE_LEN: usize = 8;
|
|
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct TaggedAccount {
|
|
pub account_type: [u8; ACCOUNT_TYPE_LEN], // "CONFIG\0\0"
|
|
pub data: AccountData,
|
|
}
|
|
|
|
impl TaggedAccount {
|
|
pub fn new_config(data: AccountData) -> Self {
|
|
let mut account_type = [0u8; ACCOUNT_TYPE_LEN];
|
|
account_type[..6].copy_from_slice(b"CONFIG");
|
|
Self { account_type, data }
|
|
}
|
|
|
|
pub fn assert_config(&self) -> ProgramResult {
|
|
let mut expected = [0u8; ACCOUNT_TYPE_LEN];
|
|
expected[..6].copy_from_slice(b"CONFIG");
|
|
|
|
if self.account_type != expected {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## PDA Security in Native Rust
|
|
|
|
### find_program_address vs create_program_address
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn init_pda(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
bump: u8,
|
|
) -> ProgramResult {
|
|
let pda_account = next_account_info(&mut accounts.iter())?;
|
|
|
|
// Using user-provided bump without validation!
|
|
let pda = Pubkey::create_program_address(
|
|
&[b"config", &[bump]],
|
|
program_id,
|
|
)?;
|
|
|
|
if pda_account.key != &pda {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Attacker could find non-canonical bump
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn init_pda(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
) -> ProgramResult {
|
|
let pda_account = next_account_info(&mut accounts.iter())?;
|
|
|
|
// Always use find_program_address to get canonical bump
|
|
let (pda, bump) = Pubkey::find_program_address(
|
|
&[b"config"],
|
|
program_id,
|
|
);
|
|
|
|
if pda_account.key != &pda {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Store the canonical bump for later use
|
|
let mut data = ConfigPda::new(bump);
|
|
data.serialize(&mut *pda_account.data.borrow_mut())?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Storing and Using Canonical Bumps
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct VaultPda {
|
|
pub discriminator: u64,
|
|
pub bump: u8,
|
|
pub authority: Pubkey,
|
|
pub balance: u64,
|
|
}
|
|
|
|
impl VaultPda {
|
|
pub fn seeds<'a>(&'a self, authority: &'a Pubkey) -> [&'a [u8]; 3] {
|
|
[b"vault", authority.as_ref(), &[self.bump]]
|
|
}
|
|
|
|
pub fn verify_pda(
|
|
&self,
|
|
pda_account: &AccountInfo,
|
|
authority: &Pubkey,
|
|
program_id: &Pubkey,
|
|
) -> ProgramResult {
|
|
let expected_pda = Pubkey::create_program_address(
|
|
&self.seeds(authority),
|
|
program_id,
|
|
)?;
|
|
|
|
if pda_account.key != &expected_pda {
|
|
return Err(ProgramError::InvalidSeeds);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### PDA Signing with invoke_signed
|
|
|
|
**Secure Pattern:**
|
|
```rust
|
|
use solana_program::program::invoke_signed;
|
|
|
|
pub fn transfer_from_pda(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
amount: u64,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let vault_pda = next_account_info(account_info_iter)?;
|
|
let destination = next_account_info(account_info_iter)?;
|
|
let authority = next_account_info(account_info_iter)?;
|
|
let system_program = next_account_info(account_info_iter)?;
|
|
|
|
// Load and validate PDA data
|
|
let vault = VaultPda::from_account_info(vault_pda)?;
|
|
vault.verify_pda(vault_pda, authority.key, program_id)?;
|
|
|
|
// Sign with PDA's seeds
|
|
let seeds = vault.seeds(authority.key);
|
|
let signer_seeds = &[&seeds[..]];
|
|
|
|
let ix = solana_program::system_instruction::transfer(
|
|
vault_pda.key,
|
|
destination.key,
|
|
amount,
|
|
);
|
|
|
|
invoke_signed(
|
|
&ix,
|
|
&[vault_pda.clone(), destination.clone(), system_program.clone()],
|
|
signer_seeds,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Preventing PDA Substitution
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn withdraw(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let vault = next_account_info(&mut accounts.iter())?;
|
|
|
|
// No validation that this is the CORRECT vault PDA
|
|
let vault_data = VaultPda::from_account_info(vault)?;
|
|
|
|
// Attacker could substitute a different vault!
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn withdraw(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let vault = next_account_info(account_info_iter)?;
|
|
let authority = next_account_info(account_info_iter)?;
|
|
|
|
// Derive expected PDA
|
|
let (expected_vault, _bump) = Pubkey::find_program_address(
|
|
&[b"vault", authority.key.as_ref()],
|
|
program_id,
|
|
);
|
|
|
|
// Validate this is the correct PDA
|
|
if vault.key != &expected_vault {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
let vault_data = VaultPda::from_account_info(vault)?;
|
|
// Safe to proceed
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Manual CPI Security
|
|
|
|
### Building AccountMeta Arrays Securely
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn dangerous_cpi(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let target_program = next_account_info(&mut accounts.iter())?;
|
|
let account1 = next_account_info(&mut accounts.iter())?;
|
|
|
|
// Missing validation - could be any program!
|
|
let ix = Instruction {
|
|
program_id: *target_program.key,
|
|
accounts: vec![
|
|
AccountMeta::new(*account1.key, false), // Wrong signer flag!
|
|
],
|
|
data: vec![],
|
|
};
|
|
|
|
invoke(&ix, &[target_program.clone(), account1.clone()])?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
use solana_program::program::invoke;
|
|
|
|
pub const EXPECTED_PROGRAM_ID: Pubkey = solana_program::pubkey!("YourProgramID111111111111111111111111111111");
|
|
|
|
pub fn secure_cpi(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let target_program = next_account_info(account_info_iter)?;
|
|
let account1 = next_account_info(account_info_iter)?;
|
|
|
|
// Validate target program ID
|
|
if target_program.key != &EXPECTED_PROGRAM_ID {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
|
|
// Correctly propagate signer/writable flags
|
|
let account_metas = vec![
|
|
AccountMeta {
|
|
pubkey: *account1.key,
|
|
is_signer: account1.is_signer,
|
|
is_writable: account1.is_writable,
|
|
},
|
|
];
|
|
|
|
let ix = Instruction {
|
|
program_id: *target_program.key,
|
|
accounts: account_metas,
|
|
data: vec![],
|
|
};
|
|
|
|
invoke(&ix, &[target_program.clone(), account1.clone()])?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Checking CPI Success
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
pub fn cpi_with_validation(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let token_program = next_account_info(account_info_iter)?;
|
|
let source = next_account_info(account_info_iter)?;
|
|
let destination = next_account_info(account_info_iter)?;
|
|
let authority = next_account_info(account_info_iter)?;
|
|
|
|
// Get balances before CPI
|
|
let source_before = source.lamports();
|
|
let dest_before = destination.lamports();
|
|
|
|
let ix = spl_token::instruction::transfer(
|
|
token_program.key,
|
|
source.key,
|
|
destination.key,
|
|
authority.key,
|
|
&[],
|
|
1000,
|
|
)?;
|
|
|
|
invoke(&ix, &[source.clone(), destination.clone(), authority.clone()])?;
|
|
|
|
// Verify state changed as expected (for native SOL transfers)
|
|
// Note: For SPL tokens, you'd need to deserialize token accounts
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Manual Serialization Security
|
|
|
|
### Borsh Serialization Pitfalls
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct Config {
|
|
pub value: u64,
|
|
pub items: Vec<Item>, // Variable length!
|
|
}
|
|
|
|
pub fn deserialize_config(account: &AccountInfo) -> ProgramResult {
|
|
// No size validation - could run out of compute!
|
|
let config = Config::try_from_slice(&account.data.borrow())?;
|
|
|
|
// Attacker could create huge Vec causing OOM
|
|
for item in &config.items {
|
|
// Process item
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub const MAX_ITEMS: usize = 100;
|
|
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct Config {
|
|
pub value: u64,
|
|
pub item_count: u32,
|
|
pub items: Vec<Item>,
|
|
}
|
|
|
|
impl Config {
|
|
pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
|
|
let data = account.data.borrow();
|
|
|
|
// Validate minimum size
|
|
if data.len() < 8 + 4 {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
let config = Self::try_from_slice(&data)
|
|
.map_err(|_| ProgramError::InvalidAccountData)?;
|
|
|
|
// Validate item count matches actual length
|
|
if config.item_count as usize != config.items.len() {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Enforce maximum items
|
|
if config.items.len() > MAX_ITEMS {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
Ok(config)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Account Data Layout Validation
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct UserAccount {
|
|
pub discriminator: u64,
|
|
pub owner: Pubkey,
|
|
pub balance: u64,
|
|
pub created_at: i64,
|
|
}
|
|
|
|
impl UserAccount {
|
|
pub const LEN: usize = 8 + 32 + 8 + 8;
|
|
|
|
pub fn from_account_info(account: &AccountInfo) -> Result<Self, ProgramError> {
|
|
let data = account.data.borrow();
|
|
|
|
// Exact size check prevents truncation attacks
|
|
if data.len() != Self::LEN {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
Self::try_from_slice(&data)
|
|
.map_err(|_| ProgramError::InvalidAccountData)
|
|
}
|
|
|
|
pub fn to_account_info(&self, account: &AccountInfo) -> ProgramResult {
|
|
let mut data = account.data.borrow_mut();
|
|
|
|
if data.len() != Self::LEN {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
self.serialize(&mut *data)
|
|
.map_err(|_| ProgramError::InvalidAccountData)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Rent and Space Management
|
|
|
|
### Rent Exemption Validation
|
|
|
|
**Secure Pattern:**
|
|
```rust
|
|
use solana_program::rent::Rent;
|
|
use solana_program::sysvar::Sysvar;
|
|
|
|
pub fn create_account(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
space: usize,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let new_account = next_account_info(account_info_iter)?;
|
|
let payer = next_account_info(account_info_iter)?;
|
|
let system_program = next_account_info(account_info_iter)?;
|
|
|
|
// Get rent sysvar
|
|
let rent = Rent::get()?;
|
|
|
|
// Calculate required lamports for rent exemption
|
|
let required_lamports = rent.minimum_balance(space);
|
|
|
|
// Validate account has enough lamports
|
|
if new_account.lamports() < required_lamports {
|
|
return Err(ProgramError::AccountNotRentExempt);
|
|
}
|
|
|
|
// Additional validation: account is rent exempt
|
|
if !rent.is_exempt(new_account.lamports(), new_account.data_len()) {
|
|
return Err(ProgramError::AccountNotRentExempt);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Account Size Calculation
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn init_account(space: usize) -> ProgramResult {
|
|
// No validation - attacker could request huge space
|
|
let ix = solana_program::system_instruction::create_account(
|
|
&payer.key,
|
|
&new_account.key,
|
|
lamports,
|
|
space as u64, // Could overflow!
|
|
program_id,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub const MIN_ACCOUNT_SIZE: usize = 128;
|
|
pub const MAX_ACCOUNT_SIZE: usize = 10_240; // 10KB
|
|
|
|
pub fn init_account(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
requested_space: usize,
|
|
) -> ProgramResult {
|
|
// Validate space within reasonable bounds
|
|
if requested_space < MIN_ACCOUNT_SIZE || requested_space > MAX_ACCOUNT_SIZE {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Ensure space alignment
|
|
let space = requested_space
|
|
.checked_next_multiple_of(8)
|
|
.ok_or(ProgramError::InvalidAccountData)?;
|
|
|
|
let rent = Rent::get()?;
|
|
let lamports = rent.minimum_balance(space);
|
|
|
|
// Safe to create account
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling in Native Rust
|
|
|
|
### Custom Error Types
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
use thiserror::Error;
|
|
use solana_program::program_error::ProgramError;
|
|
|
|
#[derive(Error, Debug, Copy, Clone)]
|
|
pub enum MyProgramError {
|
|
#[error("Invalid authority")]
|
|
InvalidAuthority,
|
|
|
|
#[error("Insufficient balance")]
|
|
InsufficientBalance,
|
|
|
|
#[error("Account already initialized")]
|
|
AlreadyInitialized,
|
|
|
|
#[error("Arithmetic overflow")]
|
|
ArithmeticOverflow,
|
|
}
|
|
|
|
impl From<MyProgramError> for ProgramError {
|
|
fn from(e: MyProgramError) -> Self {
|
|
ProgramError::Custom(e as u32)
|
|
}
|
|
}
|
|
|
|
pub fn process(accounts: &[AccountInfo]) -> Result<(), MyProgramError> {
|
|
let authority = next_account_info(&mut accounts.iter())
|
|
.map_err(|_| MyProgramError::InvalidAuthority)?;
|
|
|
|
if !authority.is_signer {
|
|
return Err(MyProgramError::InvalidAuthority);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Avoiding unwrap() and expect()
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account = accounts.get(0).unwrap(); // Panics if no accounts!
|
|
let data = account.data.borrow();
|
|
let value = u64::from_le_bytes(data[0..8].try_into().unwrap()); // Panics if not 8 bytes!
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn process(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account = accounts
|
|
.get(0)
|
|
.ok_or(ProgramError::NotEnoughAccountKeys)?;
|
|
|
|
let data = account.data.borrow();
|
|
|
|
if data.len() < 8 {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
let value = u64::from_le_bytes(
|
|
data[0..8]
|
|
.try_into()
|
|
.map_err(|_| ProgramError::InvalidAccountData)?
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Token Program Integration
|
|
|
|
### Manual Token CPI Construction
|
|
|
|
**Secure Pattern:**
|
|
```rust
|
|
use spl_token::instruction as token_instruction;
|
|
|
|
pub fn transfer_tokens(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
amount: u64,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let source_token = next_account_info(account_info_iter)?;
|
|
let dest_token = next_account_info(account_info_iter)?;
|
|
let authority = next_account_info(account_info_iter)?;
|
|
let token_program = next_account_info(account_info_iter)?;
|
|
|
|
// Validate token program
|
|
if token_program.key != &spl_token::id() {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
|
|
// Validate authority is signer
|
|
if !authority.is_signer {
|
|
return Err(ProgramError::MissingRequiredSignature);
|
|
}
|
|
|
|
// Build transfer instruction
|
|
let transfer_ix = token_instruction::transfer(
|
|
token_program.key,
|
|
source_token.key,
|
|
dest_token.key,
|
|
authority.key,
|
|
&[],
|
|
amount,
|
|
)?;
|
|
|
|
invoke(
|
|
&transfer_ix,
|
|
&[
|
|
source_token.clone(),
|
|
dest_token.clone(),
|
|
authority.clone(),
|
|
token_program.clone(),
|
|
],
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Token Account Validation
|
|
|
|
**Secure Pattern:**
|
|
```rust
|
|
use spl_token::state::Account as TokenAccount;
|
|
|
|
pub fn validate_token_account(
|
|
token_account_info: &AccountInfo,
|
|
expected_owner: &Pubkey,
|
|
expected_mint: &Pubkey,
|
|
) -> Result<TokenAccount, ProgramError> {
|
|
// Verify owned by token program
|
|
if token_account_info.owner != &spl_token::id() {
|
|
return Err(ProgramError::IncorrectProgramId);
|
|
}
|
|
|
|
// Deserialize token account
|
|
let token_account = TokenAccount::unpack(&token_account_info.data.borrow())?;
|
|
|
|
// Validate owner
|
|
if &token_account.owner != expected_owner {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Validate mint
|
|
if &token_account.mint != expected_mint {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
Ok(token_account)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Low-Level Security Patterns
|
|
|
|
### Account Reloading After External Calls
|
|
|
|
**Vulnerable:**
|
|
```rust
|
|
pub fn vulnerable_pattern(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account = next_account_info(&mut accounts.iter())?;
|
|
|
|
let balance_before = account.lamports();
|
|
|
|
// External CPI call
|
|
invoke(&some_instruction, &[account.clone()])?;
|
|
|
|
// Account data not reloaded - still using stale reference!
|
|
let balance_after = account.lamports();
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
**Secure:**
|
|
```rust
|
|
pub fn secure_pattern(accounts: &[AccountInfo]) -> ProgramResult {
|
|
let account = next_account_info(&mut accounts.iter())?;
|
|
|
|
let balance_before = account.lamports();
|
|
|
|
// External CPI call
|
|
invoke(&some_instruction, &[account.clone()])?;
|
|
|
|
// AccountInfo automatically reflects changes - lamports(), data, etc.
|
|
// are fresh after CPI
|
|
let balance_after = account.lamports();
|
|
|
|
// But if you cached deserialized data, you must reload:
|
|
let fresh_data = MyData::from_account_info(account)?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Clock and Timestamp Validation
|
|
|
|
**Secure Pattern:**
|
|
```rust
|
|
use solana_program::clock::Clock;
|
|
use solana_program::sysvar::Sysvar;
|
|
|
|
pub fn time_locked_operation(
|
|
accounts: &[AccountInfo],
|
|
unlock_timestamp: i64,
|
|
) -> ProgramResult {
|
|
// Get clock sysvar
|
|
let clock = Clock::get()?;
|
|
|
|
// Validate unlock time has passed
|
|
if clock.unix_timestamp < unlock_timestamp {
|
|
return Err(ProgramError::InvalidArgument);
|
|
}
|
|
|
|
// Proceed with operation
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Native Rust Best Practices
|
|
|
|
### Account Iteration Patterns
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
pub fn process_instruction(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
instruction_data: &[u8],
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
|
|
let authority = next_account_info(account_info_iter)?;
|
|
let config = next_account_info(account_info_iter)?;
|
|
let system_program = next_account_info(account_info_iter)?;
|
|
|
|
// Validate all expected accounts consumed
|
|
if account_info_iter.next().is_some() {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
// Validate accounts
|
|
AccountValidation::new(authority).signer()?;
|
|
AccountValidation::new(config)
|
|
.owner(program_id)?
|
|
.writable()?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### State Management Patterns
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
#[derive(BorshSerialize, BorshDeserialize)]
|
|
pub struct ProgramState {
|
|
pub version: u8,
|
|
pub is_initialized: bool,
|
|
pub authority: Pubkey,
|
|
// Add new fields at the end for upgradability
|
|
pub feature_flags: u64,
|
|
}
|
|
|
|
impl ProgramState {
|
|
pub const CURRENT_VERSION: u8 = 1;
|
|
|
|
pub fn initialize(authority: Pubkey) -> Self {
|
|
Self {
|
|
version: Self::CURRENT_VERSION,
|
|
is_initialized: true,
|
|
authority,
|
|
feature_flags: 0,
|
|
}
|
|
}
|
|
|
|
pub fn validate(&self) -> ProgramResult {
|
|
if !self.is_initialized {
|
|
return Err(ProgramError::UninitializedAccount);
|
|
}
|
|
|
|
if self.version != Self::CURRENT_VERSION {
|
|
return Err(ProgramError::InvalidAccountData);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
```
|
|
|
|
### Security.txt Integration
|
|
|
|
**Best Practice:**
|
|
```rust
|
|
#[cfg(not(feature = "no-entrypoint"))]
|
|
solana_security_txt::security_txt! {
|
|
name: "My Solana Program",
|
|
project_url: "https://github.com/myorg/myprogram",
|
|
contacts: "email:security@myorg.com,discord:myorg",
|
|
policy: "https://github.com/myorg/myprogram/blob/main/SECURITY.md",
|
|
preferred_languages: "en",
|
|
source_code: "https://github.com/myorg/myprogram",
|
|
auditors: "Auditor1, Auditor2"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Native Rust Solana programs require meticulous manual validation of all security properties:
|
|
|
|
1. **Always validate**: signer, owner, writable, key equality
|
|
2. **Use discriminators** to prevent account type confusion
|
|
3. **Store canonical bumps** and validate PDA derivation
|
|
4. **Validate CPI targets** and propagate account flags correctly
|
|
5. **Validate sizes** before deserialization
|
|
6. **Check rent exemption** for all accounts
|
|
7. **Use Result types** - never unwrap or expect
|
|
8. **Validate token accounts** completely before use
|
|
9. **Reload account data** after external calls if cached
|
|
10. **Version your state** and validate initialization
|
|
|
|
For each pattern, create reusable validation functions and leverage Rust's type system to enforce security invariants at compile time where possible.
|