4 anchor vuls (zellic)
有了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的值没有发生变化
\
\
\
\
\
\