Skip to main content

4 anchor vuls (zellic)

blog

有了anchor依旧会有些bug

1. seed collision

PDA seed 冲突可能会导致dos

1.1. 例子

#[derive(Accounts)]  
#[instruction(product_name: String)]
pub struct CreateNewProduct<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init,
payer = user,
space = 8 + Product::SIZE,
seeds = [b"product", product_name.as_ref()]
)]
pub product: Account<'info, Product>, // <-- PDA account
pub system_program: Program<'info, System>,
}

#[instruction(product_name: String)]
pub struct CreateNewBid<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds = [b"product", product_name.as_ref()],
bump = product.bump
)]
pub product: Account<'info, Product>, // <-- PDA account
#[account(
init,
payer = user,
space = 8 + Bid::SIZE,
seeds = [product_name.as_ref(), user.key().as_ref()]
)]
pub bid: Account<'info, Bid>, // <-- PDA account
pub system_program: Program<'info, System>,
}

这里有2个pda: product和bid

考虑这样一种情况:

  • 有个product name是 'pr'
  • 有个用户的key是 0x6f64756374796f7573686f756c646275797468697370726f6475637466617374

那么attacker如果先创建了一个新的product叫youshouldbuythisproductfast

此后如果user 0x6f64756374796f7573686f756c646275797468697370726f6475637466617374 想bid product pr,就无法下单了,因为这个seed已经对应了一个product.

因此这里存在pda seed冲突的问题

2. anchor ctx.remaining_accounts 误用

anchor的constraint对固定数量的account可以生效,但是无法保护可变数量的account,也即ctx.remaining_accounts

因此需要对 ctx.remaining_accounts 做额外的单独检查

  • Account Ownership 所有权
  • Account Type (anchor uses an 8 byte discriminator for this) , 类型检查
  • Account Liveness (anchor's discriminator solves this - basically 'is the data field set to 0x00…00'),活跃性分析
  • Account Address,地址
  • For PDA's ensure the bump is correct as well!,bump检查

这种数量可变的account检查,很容易出问题

3. confused deputy with CPI

CPI调用有风险。

如果program A 调用 program B,并带上了一些签名了交易的account,那program B就会把这个account 当成 signer。

这意味着program B可能创建新的account,发送lamport,或者ata等等

3.1. 例子

当pda交互时带上invoke_signed,会更复杂。

如果invoke_signed时将这个权限和seed一起传递,则可能存在三方风险。

cpi的目标program可能是调用者传入的,这也就允许攻击者使用有权限的pda来调用恶意程序。

即使是这个program是硬编码在程序里的,也存在由于upgrade导致的风险。

#[derive(Accounts)]  
pub struct RiskyInstruction<'info> {
pub lending_program: AccountInfo<'info>, // <-- no validation of account

#[account(
mut,
seeds = [ b"pool" ],
bump = pool.bump
)]
pub pool: Account<'info, Pool>
#[account]
pub caller: Signer<'info>,
pub system_program: Program<'info, System>,
}

pub fn risky_instruction(ctx: Context<RiskyInstruction>, amount: u64) -> Result<()> {
// ...

let account_infos = [
ctx.accounts.caller.to_account_info().clone(),
ctx.accounts.lending_program.to_account_info().clone(),
ctx.accounts.pool.to_account_info().clone(),
ctx.accounts.system_program.to_account_info().clone(),
];

let instruction = create_lending_instruction(
ctx.accounts.lending_program.key(), // <-- program we're calling out to
ctx.accounts.caller.key(),
ctx.accounts.pool.key(),
amount
);

// v----- this is dangerous if we don't fully trust the lending_program account
invoke_signed(
&instruction,
&account_infos,
&[&[b"pool"]]
)?;
// ...
}

4. Account Reloading(重新加载account)

anchor不会在cpi之后更新反序列化出来的account。如果需要更新,一定要重新调用anchor来reload account。这样才能更新数据

4.1. 例子

let authority_seeds = /* seeds */;  

let mint_to = MintTo {
mint: self.liquidity_mint.to_account_info(),
to: self.user.to_account_info(),
authority: self.liquidity_mint_authority.to_account_info()
};

msg!("Supply before: {}", self.liquidity_mint.supply);

anchor_spl::token::mint_to(
CpiContext::new_with_signer(
self.token_program.to_account_info(),
mint_to,
authority_seeds
),
amount
)?;

msg!("Supply after: {}", self.liquidity_mint.supply); // stays the same!

这里cpi前后,self.liquidity_mint.supply的值没有发生变化

\

\

\

\

\

\