Skip to main content

anchor sealevel-attacks

0. 链接

source code\n分析文章

1. AccountInfo 缺少signer检查

If your instruction takes in an "authority" account, make sure the account has signed the transaction.

Why? Because only the owner of the "authority" account can sign for it—but anyone can pass in the account as a non-signer.

1.1. 漏洞代码

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
#[program]
pub mod signer_authorization_insecure {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
msg!("GM {}", ctx.accounts.authority.key().to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
authority: AccountInfo<'info>,
}

1.1.1 分析

默认authority是signer,但是没有对is_signer进行检查

Don't do this—authority is not required to be a signer.

Instead, do this—authority IS required to be a signer!

1.2. 修复代码

添加is_signer检查

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod signer_authorization_secure {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
if !ctx.accounts.authority.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
msg!("GM {}", ctx.accounts.authority.key().to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
authority: AccountInfo<'info>,
}

1.3. 推荐写法

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod signer_authorization_recommended {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
msg!("GM {}", ctx.accounts.authority.key().to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
authority: Signer<'info>,
}

1.3.1. 分析

使用anchor的Signer类,其中在try_from时会自动对is_signer进行检查

#[derive(Debug, Clone)]
pub struct Signer<'info> {
info: AccountInfo<'info>,
}

impl<'info> Signer<'info> {
fn new(info: AccountInfo<'info>) -> Signer<'info> {
Self { info }
}

/// Deserializes the given `info` into a `Signer`.
#[inline(never)]
pub fn try_from(info: &AccountInfo<'info>) -> Result<Signer<'info>, ProgramError> {
if !info.is_signer {
return Err(ErrorCode::AccountNotSigner.into());
}
Ok(Signer::new(info.clone()))
}
}

2. account data matching

确认输入的account包含合法data

Make sure that passed-in accounts contain valid data. For example, if your instruction expects a token account, the token account should contain an owner, mint, amount, etc. Otherwise, you may be operating with the wrong type of account!

2.1. 漏洞代码

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod account_data_matching_insecure {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
let token = SplTokenAccount::unpack(&ctx.accounts.token.data.borrow())?;
msg!("Your account balance is: {}", token.amount);
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
token: AccountInfo<'info>,
authority: Signer<'info>,
}

2.1.1. 分析

需要确认

  • data确实是token account
  • 检查token account的owner是不是authority

Don't do this—the token account can contain arbitrary, invalid data.

Instead, do this—Anchor checks that the token account contains valid data, and that its owner is the signer of the transaction.

2.2. 修复代码

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod account_data_matching_secure {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
let token = SplTokenAccount::unpack(&ctx.accounts.token.data.borrow())?;
if ctx.accounts.authority.key != &token.owner {
return Err(ProgramError::InvalidAccountData);
}
msg!("Your acocunt balance is: {}", token.amount);
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
token: AccountInfo<'info>,
authority: Signer<'info>,
}

2.3. 推荐用法

use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod account_data_matching_recommended {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
msg!("Your account balance is: {}", ctx.accounts.token.amount);
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
#[account(constraint = authority.key == &token.owner)]
token: Account<'info, TokenAccount>,
authority: Signer<'info>,
}

2.3.1. 分析

使用anchor的TokenAccount,并在定义时添加检查 account(constraint = authority.key == &token.owner)

#[derive(Clone)]
pub struct TokenAccount(spl_token::state::Account);

impl TokenAccount {
pub const LEN: usize = spl_token::state::Account::LEN;
}

impl anchor_lang::AccountDeserialize for TokenAccount {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
spl_token::state::Account::unpack(buf).map(TokenAccount)
}
}

impl anchor_lang::AccountSerialize for TokenAccount {}

impl anchor_lang::Owner for TokenAccount {
fn owner() -> Pubkey {
ID
}
}

impl Deref for TokenAccount {
type Target = spl_token::state::Account;

fn deref(&self) -> &Self::Target {
&self.0
}
}

3. owner check

确认account被正确的program own

Make sure the passed-in accounts are owned by the correct program.

For example, if your instruction expects a token account, it should be owned by the token program.

3.1. 漏洞代码

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_error::ProgramError;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod owner_checks_insecure {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
let token = SplTokenAccount::unpack(&ctx.accounts.token.data.borrow())?;
if ctx.accounts.authority.key != &token.owner {
return Err(ProgramError::InvalidAccountData);
}
msg!("Your account balance is: {}", token.amount);
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
token: AccountInfo<'info>,
authority: Signer<'info>,
}

3.1.1. 分析

没有检查token对应的owner是否为spl_token::id,这样可能是第三方的token

Don't do this—this code doesn't check to make sure the token account is owned by the SPL token program, so it could be invalid.

Instead, do this—Anchor will verify account ownership for you!

3.2. 修复

检查token.owner是否为spl_token::ID

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program_error::ProgramError;
use anchor_lang::solana_program::program_pack::Pack;
use spl_token::state::Account as SplTokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod owner_checks_secure {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
let token = SplTokenAccount::unpack(&ctx.accounts.token.data.borrow())?;
if ctx.accounts.token.owner != &spl_token::ID {
return Err(ProgramError::InvalidAccountData);
}
if ctx.accounts.authority.key != &token.owner {
return Err(ProgramError::InvalidAccountData);
}
msg!("Your account balance is: {}", token.amount);
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
token: AccountInfo<'info>,
authority: Signer<'info>,
}

3.3. 推荐方案

使用官方的 Account<'info, TokenAccount>

use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod owner_checks_recommended {
use super::*;

pub fn log_message(ctx: Context<LogMessage>) -> ProgramResult {
msg!("Your account balance is: {}", ctx.accounts.token.amount);
Ok(())
}
}

#[derive(Accounts)]
pub struct LogMessage<'info> {
#[account(constraint = authority.key == &token.owner)]
token: Account<'info, TokenAccount>,
authority: Signer<'info>,
}

3.3.1. 分析

TokenAccount定义

pub use spl_token::ID;
...
#[derive(Clone)]
pub struct TokenAccount(spl_token::state::Account);

impl TokenAccount {
pub const LEN: usize = spl_token::state::Account::LEN;
}

impl anchor_lang::AccountDeserialize for TokenAccount {
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self, ProgramError> {
spl_token::state::Account::unpack(buf).map(TokenAccount)
}
}

impl anchor_lang::AccountSerialize for TokenAccount {}

impl anchor_lang::Owner for TokenAccount {
fn owner() -> Pubkey {
ID
}
}

impl Deref for TokenAccount {
type Target = spl_token::state::Account;

fn deref(&self) -> &Self::Target {
&self.0
}
}

Account在try_from中会检查owner

impl<'a, T: AccountSerialize + AccountDeserialize + Owner + Clone> Account<'a, T> {
/// Deserializes the given `info` into a `Account`.
#[inline(never)]
pub fn try_from(info: &'a AccountInfo<'a>) -> Result<Account<'a, T>> {
if info.owner == &system_program::ID && info.lamports() == 0 {
return Err(ErrorCode::AccountNotInitialized.into());
}
if info.owner != &T::owner() {
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, T::owner())));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(Account::new(info, T::try_deserialize(&mut data)?))
}

/// Deserializes the given `info` into a `Account` without checking
/// the account discriminator. Be careful when using this and avoid it if
/// possible.
#[inline(never)]
pub fn try_from_unchecked(info: &'a AccountInfo<'a>) -> Result<Account<'a, T>> {
if info.owner == &system_program::ID && info.lamports() == 0 {
return Err(ErrorCode::AccountNotInitialized.into());
}
if info.owner != &T::owner() {
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, T::owner())));
}
let mut data: &[u8] = &info.try_borrow_data()?;
Ok(Account::new(info, T::try_deserialize_unchecked(&mut data)?))
}
}

4. 类型混淆

Make sure one account type (e.g. User) can't be confused for another account type (e.g. Metadata).

4.1. 漏洞代码

use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod type_cosplay_insecure {
use super::*;

pub fn update_user(ctx: Context<UpdateUser>) -> ProgramResult {
let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
if ctx.accounts.user.owner != ctx.program_id {
return Err(ProgramError::IllegalOwner);
}
if user.authority != ctx.accounts.authority.key() {
return Err(ProgramError::InvalidAccountData);
}
msg!("GM {}", user.authority);
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateUser<'info> {
user: AccountInfo<'info>,
authority: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
authority: Pubkey,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Metadata {
account: Pubkey,
}

4.1.1. 漏洞分析

User和Metadata的类型定义相同,通过反序列化,不带类型检查的话,可以混淆这两个type

Don't do this—you can't tell a deserialized User account from a deserialized Metadata account

4.2. 修复

The manual way to fix this is by adding a "discriminant" to both accounts, i.e. something that allows you to distinguish between them

use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod type_cosplay_secure {
use super::*;

pub fn update_user(ctx: Context<UpdateUser>) -> ProgramResult {
let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
if ctx.accounts.user.owner != ctx.program_id {
return Err(ProgramError::IllegalOwner);
}
if user.authority != ctx.accounts.authority.key() {
return Err(ProgramError::InvalidAccountData);
}
if user.discriminant != AccountDiscriminant::User {
return Err(ProgramError::InvalidAccountData);
}
msg!("GM {}", user.authority);
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateUser<'info> {
user: AccountInfo<'info>,
authority: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
discriminant: AccountDiscriminant,
authority: Pubkey,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct Metadata {
discriminant: AccountDiscriminant,
account: Pubkey,
}

#[derive(BorshSerialize, BorshDeserialize, PartialEq)]
pub enum AccountDiscriminant {
User,
Metadata,
}

4.3. 推荐方案

The recommended way to fix this is by just using Anchor's #[account] macro, which will automatically add an 8-byte discriminator to the start of the account. Much easier!

use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod type_cosplay_recommended {
use super::*;

pub fn update_user(ctx: Context<UpdateUser>) -> ProgramResult {
msg!("GM {}", ctx.accounts.user.authority);
Ok(())
}
}

#[derive(Accounts)]
pub struct UpdateUser<'info> {
#[account(has_one = authority)]
user: Account<'info, User>,
authority: Signer<'info>,
}

#[account]
pub struct User {
authority: Pubkey,
}

#[account]
pub struct Metadata {
account: Pubkey,
}

4.3.1. 分析

discriminator 是一种用于标识和区分不同类型账户的字段。在 Anchor 中,使用 #[account] 属性注解的结构体可以包含一个 discriminator 字段。\n文档

#[account]定义

#[proc_macro_attribute]
pub fn account(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
...

let discriminator: proc_macro2::TokenStream = {
// Namespace the discriminator to prevent collisions.
let discriminator_preimage = {
// For now, zero copy accounts can't be namespaced.
if namespace.is_empty() {
format!("account:{}", account_name)
} else {
format!("{}:{}", namespace, account_name)
}
};

let mut discriminator = [0u8; 8];
discriminator.copy_from_slice(
&anchor_syn::hash::hash(discriminator_preimage.as_bytes()).to_bytes()[..8],
);
format!("{:?}", discriminator).parse().unwrap()
};

这里将namespace和account_name拼起来,做一个sha256,并取前8个字节作为descriminator。

随后在序列化和反序列化时,会用到descriminator:

  • 序列化时,自动在最开始的8个字节中写入descriminator。
  • 反序列化时,会检查反序列化的account前8个字节是否为预期的descriminator。
               #[automatically_derived]
impl #impl_gen anchor_lang::AccountSerialize for #account_name #type_gen #where_clause {
fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
writer.write_all(&#discriminator).map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotSerialize)?;
AnchorSerialize::serialize(
self,
writer
)
.map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotSerialize)?;
Ok(())
}
}

#[automatically_derived]
impl #impl_gen anchor_lang::AccountDeserialize for #account_name #type_gen #where_clause {
fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
if buf.len() < #discriminator.len() {
return Err(anchor_lang::__private::ErrorCode::AccountDiscriminatorNotFound.into());
}
let given_disc = &buf[..8];
if &#discriminator != given_disc {
return Err(anchor_lang::__private::ErrorCode::AccountDiscriminatorMismatch.into());
}
Self::try_deserialize_unchecked(buf)
}

fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
let mut data: &[u8] = &buf[8..];
AnchorDeserialize::deserialize(&mut data)
.map_err(|_| anchor_lang::__private::ErrorCode::AccountDidNotDeserialize.into())
}
}

#[automatically_derived]
impl #impl_gen anchor_lang::Discriminator for #account_name #type_gen #where_clause {
fn discriminator() -> [u8; 8] {
#discriminator
}
}

5. 初始化

初始化account时,确认好account的类型

This is similar to the previous vulnerability—when initializing accounts, make sure you account for the discriminator.

E.g., you don't want to initialize the wrong type of account. And you may not want to re-initialize an already initialized account.

5.1. 漏洞代码

use anchor_lang::prelude::*; 
use borsh::{BorshDeserialize, BorshSerialize};
use std::ops::DerefMut;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod initialization_insecure {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();

user.authority = ctx.accounts.authority.key();

let mut storage = ctx.accounts.user.try_borrow_mut_data()?;
user.serialize(storage.deref_mut()).unwrap();
Ok(())
}
}

/*
- reinitialize
- create and dont initialize
- passing previously initialzed accounts from other programs
(e.g. token program => need to check delegate and authority)
*/

#[derive(Accounts)]
pub struct Initialize<'info> {
user: AccountInfo<'info>,
authority: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
authority: Pubkey,
}

5.1.1. 分析

这个account可以是其他account类型。

并且也能重新初始化这些已经初始化过的account

Don't do this—the user account could be another account type (since there is no discriminator).

And this also lets people re-initialize previously initialized accounts.

#[account(init)]会创建一个新的account,并设置其discriminator.

Instead, do this—using #[account(init)] will create a new account and set its account discriminator.

5.2. 修复代码

use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};
use std::ops::DerefMut;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod reinitialization_secure_recommended {
use super::*;

pub fn initialize(ctx: Context<Initialize>) -> ProgramResult {
let mut user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
if !user.discriminator {
return Err(ProgramError::InvalidAccountData);
}

user.authority = ctx.accounts.authority.key();
user.discriminator = true;

let mut storage = ctx.accounts.user.try_borrow_mut_data()?;
user.serialize(storage.deref_mut()).unwrap();

msg!("GM");
Ok(())
}
}

#[derive(Accounts)]
pub struct Initialize<'info> {
user: AccountInfo<'info>,
authority: Signer<'info>,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
discriminator: bool,
authority: Pubkey,
}

5.3. 推荐方案

添加#[account(init)]

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod reinitialization_4 {
use super::*;

pub fn init(_ctx: Context<Init>) -> ProgramResult {
msg!("GM");
Ok(())
}
}


#[derive(Accounts)]
pub struct Init<'info> {
#[account(init, payer = authority, space = 8+32)]
user: Account<'info, User>,
#[account(mut)]
authority: Signer<'info>,
system_program: Program<'info, System>,
}

#[account]
pub struct User {
authority: Pubkey,
}

6. 任意cpi调用

When performing CPIs, make sure you're invoking the correct program.

6.1. 漏洞代码

use anchor_lang::prelude::*;
use anchor_lang::solana_program;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod arbitrary_cpi_insecure {
use super::*;

pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
solana_program::program::invoke(
&spl_token::instruction::transfer(
ctx.accounts.token_program.key,
ctx.accounts.source.key,
ctx.accounts.destination.key,
ctx.accounts.authority.key,
&[],
amount,
)?,
&[
ctx.accounts.source.clone(),
ctx.accounts.destination.clone(),
ctx.accounts.authority.clone(),
],
)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: AccountInfo<'info>,
destination: AccountInfo<'info>,
authority: AccountInfo<'info>,
token_program: AccountInfo<'info>,
}

6.1.1. 分析

没有检查token_program是否为spl_token::ID.

Don't do this—the token program account gets passed in by the user, and could actually be some other program.

6.2. 修复代码

The manual way to fix this is checking to make sure the token program account has the right address.

use anchor_lang::prelude::*;
use anchor_lang::solana_program;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod arbitrary_cpi_secure {
use super::*;

pub fn cpi_secure(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
if &spl_token::ID != ctx.accounts.token_program.key {
return Err(ProgramError::IncorrectProgramId);
}
solana_program::program::invoke(
&spl_token::instruction::transfer(
ctx.accounts.token_program.key,
ctx.accounts.source.key,
ctx.accounts.destination.key,
ctx.accounts.authority.key,
&[],
amount,
)?,
&[
ctx.accounts.source.clone(),
ctx.accounts.destination.clone(),
ctx.accounts.authority.clone(),
],
)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: AccountInfo<'info>,
destination: AccountInfo<'info>,
authority: AccountInfo<'info>,
token_program: AccountInfo<'info>,
}

6.3. 推荐方案

anchor自带的Program<'info, Token> 会自动检查id是否为spl token program

The recommended way to fix this is by using Anchor's wrapper of the SPL token program.

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod arbitrary_cpi_recommended {
use super::*;

pub fn cpi(ctx: Context<Cpi>, amount: u64) -> ProgramResult {
token::transfer(ctx.accounts.transfer_ctx(), amount)
}
}

#[derive(Accounts)]
pub struct Cpi<'info> {
source: Account<'info, TokenAccount>,
destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> Cpi<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.source.to_account_info(),
to: self.destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

6.3.1. 分析

#[derive(Accounts)]
pub struct Cpi<'info> {
source: Account<'info, TokenAccount>,
destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> Cpi<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.source.to_account_info(),
to: self.destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

这里的program是预先定义好的 Program<'info, Token>

Token定义为

pub use spl_token::ID;
...
#[derive(Clone)]
pub struct Token;

impl anchor_lang::Id for Token {
fn id() -> Pubkey {
ID
}
}

Pragram中有

impl<'a, T: Id + Clone> Program<'a, T> {
...

/// Deserializes the given `info` into a `Program`.
#[inline(never)]
pub fn try_from(info: &AccountInfo<'a>) -> Result<Program<'a, T>, ProgramError> {
if info.key != &T::id() {
return Err(ErrorCode::InvalidProgramId.into());
}
...
}

Program在从Token AccountInfo转换过来时,会检查token_pragram.key 是否等于Token::id()。

7. 重复的mut account

对于可以修改的account,要避免用户一次性传入两个一样account。

If your program takes in two mutable accounts of the same type, make sure people don't pass in the same account twice.

7.1. 漏洞代码

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod duplicate_mutable_accounts_insecure {
use super::*;

pub fn update(ctx: Context<Update>, a: u64, b: u64) -> ProgramResult {
let user_a = &mut ctx.accounts.user_a;
let user_b = &mut ctx.accounts.user_b;

user_a.data = a;
user_b.data = b;
Ok(())
}
}

#[derive(Accounts)]
pub struct Update<'info> {
user_a: Account<'info, User>,
user_b: Account<'info, User>,
}

#[account]
pub struct User {
data: u64,
}

7.1.1. 分析

这里user_a和user_b可能是同一个account

Don't do this—user_a and user_b may be the same account.

7.2. 修复代码

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod duplicate_mutable_accounts_secure {
use super::*;

pub fn update(ctx: Context<Update>, a: u64, b: u64) -> ProgramResult {
if ctx.accounts.user_a.key() == ctx.accounts.user_b.key() {
return Err(ProgramError::InvalidArgument)
}
let user_a = &mut ctx.accounts.user_a;
let user_b = &mut ctx.accounts.user_b;

user_a.data = a;
user_b.data = b;
Ok(())
}
}

#[derive(Accounts)]
pub struct Update<'info> {
user_a: Account<'info, User>,
user_b: Account<'info, User>,
}

#[account]
pub struct User {
data: u64,
}

7.3. 推荐方案

Instead, use Anchor to verify that the two accounts are different.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod duplicate_mutable_accounts_recommended {
use super::*;

pub fn update(ctx: Context<Update>, a: u64, b: u64) -> ProgramResult {
let user_a = &mut ctx.accounts.user_a;
let user_b = &mut ctx.accounts.user_b;

user_a.data = a;
user_b.data = b;
Ok(())
}
}

#[derive(Accounts)]
pub struct Update<'info> {
#[account(constraint = user_a.key() != user_b.key())]
user_a: Account<'info, User>,
user_b: Account<'info, User>,
}

#[account]
pub struct User {
data: u64,
}

8. Bump seed标准化

验证PDA的时候,应该使用find_program_address ,不能使用 create_program_address

Often, you want to have a single PDA associated with a program ID + a set of seeds.

For example, associated token accounts (ATAs).

Thus, when verifying PDAs, you should use find_program_address instead of create_program_address.

8.1. 漏洞代码

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod bump_seed_canonicalization_insecure {
use super::*;

pub fn set_value(ctx: Context<BumpSeed>, key: u64, new_value: u64, bump: u8) -> ProgramResult {
let address =
Pubkey::create_program_address(&[key.to_le_bytes().as_ref(), &[bump]], ctx.program_id)?;
if address != ctx.accounts.data.key() {
return Err(ProgramError::InvalidArgument);
}

ctx.accounts.data.value = new_value;

Ok(())
}
}

#[derive(Accounts)]
pub struct BumpSeed<'info> {
data: Account<'info, Data>,
}

#[account]
pub struct Data {
value: u64,
}

Don't do this—since the bump is passed in by the user (and not verified), set_value could operate on multiple PDAs associated with the program ID + the set of seeds.

不要依赖用户输入的bump。

8.2. 修复代码

Instead, do this—both the PDA address and bump are verified, which means set_value will only operate on one "canonical" PDA.

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod bump_seed_canonicalization_secure {
use super::*;

pub fn set_value_secure(
ctx: Context<BumpSeed>,
key: u64,
new_value: u64,
bump: u8,
) -> ProgramResult {
let (address, expected_bump) =
Pubkey::find_program_address(&[key.to_le_bytes().as_ref()], ctx.program_id);

if address != ctx.accounts.data.key() {
return Err(ProgramError::InvalidArgument);
}
if expected_bump != bump {
return Err(ProgramError::InvalidArgument);
}

ctx.accounts.data.value = new_value;
Ok(())
}
}

#[derive(Accounts)]
pub struct BumpSeed<'info> {
data: Account<'info, Data>,
}

#[account]
pub struct Data {
value: u64,
}

8.3. 推荐方案

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod bump_seed_canonicalization_recommended {
use super::*;

pub fn set_value(ctx: Context<BumpSeed>, key: u64, new_value: u64) -> ProgramResult {
ctx.accounts.data.value = new_value;
Ok(())
}
}

#[derive(Accounts)]
#[instruction(key: u64)]
pub struct BumpSeed<'info> {
// Note a subtle pattern that is not displayed here.
//
// Usually, the usage of PDAs is broken into two parts:
//
// 1) allocation via `#[account(init, seeds = [...], bump)]`
// 2) using the account via `#[account(init, seeds = [...], bump = data.bump)]
//
// When using a PDA, it's usually recommend to store the bump seed in the
// account data, so that you can use it as demonstrated in 2), which will
// provide a more efficient check.
#[account(seeds = [key.to_le_bytes().as_ref()], bump)]
data: Account<'info, Data>,
}

#[account]
pub struct Data {
value: u64,
}

8.3.1. 分析

anchor会根据指定的seeds和bump,自动生成 pda校验的代码

            ConstraintToken::Bump(c) => self.add_bump(c),
ConstraintToken::ProgramSeed(c) => self.add_program_seed(c),

生成计算pda与比较代码。

fn generate_constraint_seeds(f: &Field, c: &ConstraintSeedsGroup) -> proc_macro2::TokenStream {
if c.is_init {
// Note that for `#[account(init, seeds)]`, the seed generation and checks is checked in
// the init constraint find_pda/validate_pda block, so we don't do anything here and
// return nothing!
quote! {}
} else {
let name = &f.ident;
let name_str = name.to_string();

let s = &mut c.seeds.clone();

let deriving_program_id = c
.program_seed
.clone()
// If they specified a seeds::program to use when deriving the PDA, use it.
.map(|program_id| quote! { #program_id.key() })
// Otherwise fall back to the current program's program_id.
.unwrap_or(quote! { __program_id });

// If the seeds came with a trailing comma, we need to chop it off
// before we interpolate them below.
if let Some(pair) = s.pop() {
s.push_value(pair.into_value());
}

let maybe_seeds_plus_comma = (!s.is_empty()).then(|| {
quote! { #s, }
});
let bump = if f.is_optional {
quote!(Some(__bump))
} else {
quote!(__bump)
};

// Not init here, so do all the checks.
let define_pda = match c.bump.as_ref() {
// Bump target not given. Find it.
None => quote! {
let (__pda_address, __bump) = Pubkey::find_program_address(
&[#maybe_seeds_plus_comma],
&#deriving_program_id,
);
__bumps.#name = #bump;
},
// Bump target given. Use it.
Some(b) => quote! {
let __pda_address = Pubkey::create_program_address(
&[#maybe_seeds_plus_comma &[#b][..]],
&#deriving_program_id,
).map_err(|_| anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str))?;
},
};
quote! {
// Define the PDA.
#define_pda

// Check it.
if #name.key() != __pda_address {
return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintSeeds).with_account_name(#name_str).with_pubkeys((#name.key(), __pda_address)));
}
}
}

9. PDA sharing

9.1. 漏洞代码

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod pda_sharing_insecure {
use super::*;

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> ProgramResult {
let amount = ctx.accounts.vault.amount;
let seeds = &[ctx.accounts.pool.mint.as_ref(), &[ctx.accounts.pool.bump]];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(has_one = vault, has_one = withdraw_destination)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}

9.1.1. 分析

这里pda的seed生成与用户无关,是个全局共享的,所以任何人都能把其中的token取完

let seeds = &[ctx.accounts.pool.mint.as_ref(), &[ctx.accounts.pool.bump]];

9.2. 修复代码

将pool.mint换成withdraw_destinaton

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod pda_sharing_secure {
use super::*;

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> ProgramResult {
let amount = ctx.accounts.vault.amount;
let seeds = &[
ctx.accounts.pool.withdraw_destination.as_ref(),
&[ctx.accounts.pool.bump],
];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(has_one = vault, has_one = withdraw_destination)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}

9.3. 推荐写法

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount};

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod pda_sharing_recommended {
use super::*;

pub fn withdraw_tokens(ctx: Context<WithdrawTokens>) -> ProgramResult {
let amount = ctx.accounts.vault.amount;
let seeds = &[
ctx.accounts.pool.withdraw_destination.as_ref(),
&[ctx.accounts.pool.bump],
];
token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount)
}
}

#[derive(Accounts)]
pub struct WithdrawTokens<'info> {
#[account(
has_one = vault,
has_one = withdraw_destination,
seeds = [withdraw_destination.key().as_ref()],
bump = pool.bump,
)]
pool: Account<'info, TokenPool>,
vault: Account<'info, TokenAccount>,
withdraw_destination: Account<'info, TokenAccount>,
authority: Signer<'info>,
token_program: Program<'info, Token>,
}

impl<'info> WithdrawTokens<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.vault.to_account_info(),
to: self.withdraw_destination.to_account_info(),
authority: self.authority.to_account_info(),
};
CpiContext::new(program, accounts)
}
}

#[account]
pub struct TokenPool {
vault: Pubkey,
mint: Pubkey,
withdraw_destination: Pubkey,
bump: u8,
}

9.3.1. 分析

可以在注解account中指定 seeds和bump

10.

11. sysvar address检查

11.1. 漏洞源码

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod insecure {
use super::*;

pub fn check_sysvar_address(ctx: Context<CheckSysvarAddress>) -> Result<()> {
msg!("Rent Key -> {}", ctx.accounts.rent.key().to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct CheckSysvarAddress<'info> {
rent: AccountInfo<'info>,
}

11.2. 修复代码

use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod secure {
use super::*;

pub fn check_sysvar_address(ctx: Context<CheckSysvarAddress>) -> Result<()> {
require_eq!(ctx.accounts.rent.key(), sysvar::rent::ID);
msg!("Rent Key -> {}", ctx.accounts.rent.key().to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct CheckSysvarAddress<'info> {
rent: AccountInfo<'info>,
}

11.3. 推荐

use anchor_lang::prelude::*;

declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");

#[program]
pub mod recommended {
use super::*;

pub fn check_sysvar_address(ctx: Context<CheckSysvarAddress>) -> Result<()> {
msg!("Rent Key -> {}", ctx.accounts.rent.key().to_string());
Ok(())
}
}

#[derive(Accounts)]
pub struct CheckSysvarAddress<'info> {
rent: Sysvar<'info, Rent>,
}

11.3.1. 分析

anchor中限制了T必须为solana_program::sysvar::Sysvar

pub struct Sysvar<'info, T: solana_program::sysvar::Sysvar> {
info: AccountInfo<'info>,
account: T,
}

impl<'info, T: solana_program::sysvar::Sysvar + fmt::Debug> fmt::Debug for Sysvar<'info, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sysvar")
.field("info", &self.info)
.field("account", &self.account)
.finish()
}
}

impl<'info, T: solana_program::sysvar::Sysvar> Sysvar<'info, T> {
pub fn from_account_info(acc_info: &AccountInfo<'info>) -> Result<Sysvar<'info, T>> {
match T::from_account_info(acc_info) {
Ok(val) => Ok(Sysvar {
info: acc_info.clone(),
account: val,
}),
Err(_) => Err(ErrorCode::AccountSysvarMismatch.into()),
}
}
}

impl<'info, T: solana_program::sysvar::Sysvar> Clone for Sysvar<'info, T> {
fn clone(&self) -> Self {
Self {
info: self.info.clone(),
account: T::from_account_info(&self.info).unwrap(),
}
}
}

impl<'info, T: solana_program::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, T> {
fn try_accounts(
_program_id: &Pubkey,
accounts: &mut &[AccountInfo<'info>],
_ix_data: &[u8],
_bumps: &mut BTreeMap<String, u8>,
_reallocs: &mut BTreeSet<Pubkey>,
) -> Result<Self> {
if accounts.is_empty() {
return Err(ErrorCode::AccountNotEnoughKeys.into());
}
let account = &accounts[0];
*accounts = &accounts[1..];
Sysvar::from_account_info(account)
}
}

在solana_program中,from_account_info对类型id做了检查

pub trait SysvarId {
fn id() -> Pubkey;

fn check_id(pubkey: &Pubkey) -> bool;
}

// Sysvar utilities
pub trait Sysvar:
SysvarId + Default + Sized + serde::Serialize + serde::de::DeserializeOwned
{
...
/// Deserializes a sysvar from its `AccountInfo`.
///
/// # Errors
///
/// If `account_info` does not have the same ID as the sysvar
/// this function returns [`ProgramError::InvalidArgument`].
fn from_account_info(account_info: &AccountInfo) -> Result<Self, ProgramError> {
if !Self::check_id(account_info.unsigned_key()) {
return Err(ProgramError::InvalidArgument);
}
bincode::deserialize(&account_info.data.borrow()).map_err(|_| ProgramError::InvalidArgument)
}
...
}