Common pitfalls
原文:Solana Smart Contracts: Common Pitfalls and How to Avoid Them
1. Missing ownership check
一定要检查AccountInfo::owner
合约只能信任自己own的account
solana标准account
pub struct AccountInfo<'a> {
// [...]
/// Program that owns this account
pub owner: &'a Pubkey,
// [...]
}
owner不是自己的account,都可能有恶意数据,不可信任
1.1. 案例
1.1.1. 源码
只有admin可以withdraw
fn withdraw_token_restricted(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(())
}
1.1.2. 漏洞
没有检查config是否属于合约,因此攻击者可提供一个恶意config,其中包含一个恶意admin
1.1.3. 修复
添加ownership检查
fn withdraw_token_restricted(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.owner != program_id {
return Err(ProgramError::InvalidConfigAccount);
}
if config.admin != admin.pubkey() {
return Err(ProgramError::InvalidAdminAccount);
}
// ...
// Transfer funds from vault to admin using vault_authority
// ...
Ok(())
}
An even better fix than the above is to introduce a different type for accounts that have already been verified to be program-owned and to then ensure that the contract does any relevant computations only with accounts of that type.
2. Missing signer check
instruction只能由特定的用户访问,需要通过AccountInfo::is_signer验证call是否被正确的签发
2.1. 案例
2.1.1. 漏洞源码
fn update_admin(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 new_admin = next_account_info(account_iter)?;
// ...
// Validate the config account...
// ...
if admin.pubkey() != config.admin {
return Err(ProgramError::InvalidAdminAccount);
}
config.admin = new_admin.pubkey();
Ok(())
}
2.1.2. 漏洞分析
只检查了admin是不是当前config中制定的。但是没有检查instruction是否由admin 签名。
可以直接由任意用户调用,只要提供正确的admin即可
2.1.3. 漏洞修补
检查是否由admin 签发
fn update_admin(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 new_admin = next_account_info(account_iter)?;
// ...
// Validate the config account...
// ...
if admin.pubkey() != config.admin {
return Err(ProgramError::InvalidAdminAccount);
}
// check that the current admin has signed this operation
if !admin.is_signer {
return Err(ProgramError::MissingSigner);
}
config.admin = new_admin.pubkey();
Ok(())
}
3. Integer overflow & underflow
整数运算是使用check math和check cast,避免潜在的非预期行为
rust release版本的代码中,整数运算的溢出可能会直接二进制wrap 文档
When you're compiling in release mode with the
--releaseflag, Rust does not include checks for integer overflow that cause panics. Instead, if overflow occurs, Rust performs two's complement wrapping. In short, values greater than the maximum value the type can hold "wrap around" to the minimum of the values the type can hold. In the case of au8, 256 becomes 0, 257 becomes 1, and so on. The program won't panic, but the variable will have a value that probably isn't what you were expecting it to have.
cargo build-bpf 是直接编译出release mode
3.0. 整数类型转换
u64 的值通过as u32转换时,可能会截断,导致意想不到的bug
3.1. 案例
3.1.1. 漏洞代码
let FEE: u32 = 1000;
fn withdraw_token(program_id: &Pubkey, accounts: &[AccountInfo], amount: u32) -> ProgramResult {
// ...
// deserialize & validate user and vault accounts
// ...
if amount + FEE > vault.user_balance[user_id] {
return Err(ProgramError::AttemptToWithdrawTooMuch);
}
// ...
// Transfer `amount` many tokens from vault to user-controlled account ...
// ...
Ok(())
}
3.1.2. 漏洞分析
amount 给个u32::max-100左右的值就能溢出了,加法结果899,满足检查,可以继续取钱
3.1.3. 修复
添加check_add
let FEE: u32 = 1000;
fn withdraw_token(program_id: &Pubkey, accounts: &[AccountInfo], amount: u32) -> ProgramResult {
// ...
// deserialize & validate user and vault accounts
// ...
if amount.checked_add(FEE).ok_or(ProgramError::InvalidArgument)? > vault.user_balance[user_id] {
return Err(ProgramError::AttemptToWithdrawTooMuch);
}
// ...
// Transfer `amount` many tokens from vault to user-controlled account ...
// ...
Ok(())
}
4. Arbitrary signed program invocation
一定要验证所有通过invoke_signed()调用的program
4.1. 漏洞代码
pub fn process_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let vault = next_account_info(account_info_iter)?;
let vault_authority = next_account_info(account_info_iter)?;
let destination = next_account_info(account_info_iter)?;
let token_program = next_account_info(account_info_iter)?;
// ...
// get signer seeds, validate account owners and signers,
// and verify that the user can withdraw the supplied amount
// ...
// invoke unverified token_program
invoke_signed(
&spl_token::instruction::transfer(
&token_program.key,
&vault.key,
&destination.key,
&vault_authority.key,
&[&vault_authority.key],
amount,
)?,
&[
vault.clone(),
destination.clone(),
vault_owner_info.clone(),
token_program.clone(),
],
&[&seeds],
)?;
Ok(())
}
4.2. 漏洞分析
spl program token_program是攻击者可控的。这里program中实现的transfer可以不一定只转amount的token,可以直接清空整个vault
4.3. 修复
检查token_program确实是spl token
pub fn process_withdraw(program_id: &Pubkey, accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let vault = next_account_info(account_info_iter)?;
let vault_authority = next_account_info(account_info_iter)?;
let destination = next_account_info(account_info_iter)?;
let token_program = next_account_info(account_info_iter)?;
// ...
// get signer seeds, validate account owners and signers,
// and verify that the user can withdraw the supplied amount
// ...
// verify that token_program is in fact the official spl token program
if token_program.key != &spl_token::id() {
return Err(ProgramError::InvalidTokenProgram);
}
invoke_signed(
&spl_token::instruction::transfer(
&token_program.key,
&vault.key,
&destination.key,
&vault_authority.key,
&[&vault_authority.key],
amount,
)?,
&[
vault.clone(),
destination.clone(),
vault_owner_info.clone(),
token_program.clone(),
],
&[&seeds],
)?;
Ok(())
}
solana在spl的invocation中加入了一个硬编码检查,通过program_id确认调用的是不是SPL program。
但是当调用非spl 或旧版spl program时,这个问题依旧存在。
建议一定加入上面的检查
5. solana account confusions
用户可以输入任意类型的data,即使account属于contract,但是也要保证account type要对,需要对account进行类型检查,否则可能会有类型混淆
5.0. account版本
account代码可能会升级,比如添加或修改field,必须确保old和new data format时兼容的,可以引入一个version field 或 为每个change定义一个新的类型 <OldTypeName>v2
5.1. 漏洞代码
// ------- Account Types --------
pub struct Config {
pub admin: Pubkey,
pub fee: u32,
pub user_count: u32,
}
pub struct User {
pub user_authority: Pubkey,
pub balance: u64,
}
// ------- Helper functions --------
fn unpack_config(account: &AccountInfo) -> Result<Config, ProgramError> {
let mut config: Config = deserialize(&mut account.data.borrow())?;
return config;
}
// ------- Contract Instructions ---------
fn create_user(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let user = next_account_info(account_iter)?;
// ...
// Initialize a User struct, set user_authority
// to user and set balance to 0
// ...
Ok(())
}
fn withdraw_tokens(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 = unpack_config(next_account_info(account_iter)?)?;
let vault_authority = next_account_info(account_iter)?;
if config.owner != program_id {
return Err(ProgramError::InvalidConfigAccount);
}
if config.admin != admin.pubkey() {
return Err(ProgramError::InvalidAdminAccount);
}
// ...
// Transfer funds from vault to admin using vault_authority
// ...
Ok(())
}
5.2. 漏洞分析
solana account data是不知道type的,只是一串bytes
这里可以通过create_user创建一个program-owned的user,并且user_authority时攻击者可控的。
这样就能通过一个user account来混淆出一个config account\nUser account
user_authority: 12345AAAAAAAAAAAAAAAAAAAAAAAAAAA;
balance: 0x1111111111111111
Config account
admin: 12345AAAAAAAAAAAAAAAAAAAAAAAAAAA
user_count: 0x11111111
fee: 0x11111111
5.3. 修复
给每个account添加一个type field
// ------- Account Types --------
pub struct Config {
pub TYPE: u8, // <-- should contain a unique identifier for this account type
pub admin: Pubkey,
pub fee: u32,
pub user_count: u32,
}
pub struct User {
pub TYPE: u8, // <-- should contain a unique identifier for this account type
pub user_authority: Pubkey,
pub balance: u64,
}
反序列化之后检查type
// ------- Helper functions --------
fn unpack_config(account: &AccountInfo) -> Result<Config, ProgramError> {
let mut config: Config = deserialize(&mut account.data.borrow())?;
if config.TYPE != Types::ConfigType {
return Err(ProgramError::InvalidAccountType);
}
return config;
}