本文详细介绍了Solana平台的安全最佳实践,包括运行时策略、所有权及账户修改、不可变性、零初始化、交易一致性等多个方面。提供了Rust和Anchor语言中实现这些最佳实践的具体代码示例,强调了开发过程中需要采取的多种安全措施,以防止攻击和确保程序的可靠性和安全性。
CreateAccount
和 Assign
保证当一个账户被分配给程序时,账户的数据被零初始化。CreateAccountWithSeed
创建的,在这种情况下,账户的地址/pubkey 不存在相应的私钥。CreateAccount
和 Assign
保证当账户被分配给程序时,其数据被零初始化。CreateAccount
指令分配内存,然后再将其传递给另一个程序。CreateAccount
指令可以与对程序本身的调用组合成一个单一的交易。使用 Signer Checks
验证特定账户是否已签署交易。没有适当的签名者检查,账户可能会执行它们不应被授权执行的指令。
要在 Rust 中实现签名者检查,只需检查一个账户的 is_signer
属性是否为 true
if !ctx.accounts.authority.is_signer {
return Err(ProgramError::MissingRequiredSignature.into());
}
在 Anchor 中,你可以在账户验证结构中使用 Signer
账户类型,使 Anchor 自动对给定账户执行签名者检查
##[derive(Accounts)]
pub struct UpdateUserData<'info> {
// 执行签名者检查
pub user: Signer<'info>,
}
使用 Owner Checks
验证账户是否由预期程序拥有。没有适当的所有者检查,意外程序拥有的账户可能会在一条指令中被使用。
要在 Rust 中实现所有者检查,只需检查一个账户的所有者是否与预期的程序 ID 匹配
if ctx.accounts.account.owner != ctx.program_id {
return Err(ProgramError::IncorrectProgramId.into());
}
Anchor 程序账户类型实现了 Owner
trait,这使得 Account<'info, T>
包装器可以自动验证程序所有权
##[derive(Accounts)]
pub struct UpdateUserData<'info> {
pub user: Signer<'info>,
// 执行所有权检查
pub user_data: Account<'info, UserData>,
}
如果账户的所有者应为当前执行程序之外的其他内容,Anchor 允许你显式定义账户的所有者。
使用 Data Validation checks
验证账户数据是否与预期值匹配。没有适当的数据验证检查,意外账户可能会在一条指令中被使用。
要在 Rust 中实现数据验证检查,只需将账户上存储的数据与预期值进行比较。
if ctx.accounts.user.key() != ctx.accounts.user_data.user {
return Err(ProgramError::InvalidAccountData.into());
}
在 Anchor 中,你可以使用 constraint
检查给定表达式是否计算为真。或者,你可以使用 has_one
检查存储在账户中的目标账户字段是否与 Accounts
结构中的一个账户的密钥匹配。
##[derive(Accounts)]
pub struct UpdateUserData<'info> {
pub user: Signer<'info>,
#[account(constraint = user_data.authority = user.key())]
pub user_data: Account<'info, UserData>,
}
使用 账户鉴别器或初始化标志
检查一个账户是否已被初始化,以防止账户被重新初始化并覆盖现有账户数据。
要在普通 Rust 中防止账户重新初始化,请使用 is_initialized
标志初始化账户,并在初始化账户时检查其是否已被设置为 true。
if account.is_initialized {
return Err(ProgramError::AccountAlreadyInitialized.into());
}
为了简化此过程,请使用 Anchor 的 init
约束通过 CPI 创建账户并设置其鉴别器。
##[derive(Accounts)]
pub struct UpdateUserData<'info> {
pub user: Signer<'info>,
// 如果账户已经初始化,init 将返回错误
#[account(init)]
pub user_data: Account<'info, UserData>,
}
当一条指令需要两个相同类型的可变账户时,攻击者可以将同一个账户传入两次,从而导致账户以意外的方式被修改。
要在 Rust 中检查重复可变账户,只需比较两个账户的公钥,并在它们相同的情况下抛出错误。
if ctx.accounts.account_one.key() == ctx.accounts.account_two.key() {
return Err(ProgramError::InvalidArgument)
}
在 Anchor 中,你可以使用约束为账户添加一个明确的约束,检查它与另一个账户是否不同。
##[derive(Accounts)]
pub struct UpdateUserData<'info> {
pub user: Signer<'info>,
#[account(
mut,
constraint = user_data.key() != different_user_data.key()
)]
pub user_data: Account<'info, UserData>,
#[account(mut)]
pub different_user_data: Account<'info, UserData>,
}
使用鉴别器来区分不同的账户类型
要在 Rust 中实现鉴别器,请在账户结构中包含一个字段以表示账户类型
##[derive(BorshSerialize, BorshDeserialize)]
pub struct User {
discriminant: AccountDiscriminant,
user: Pubkey,
}
##[derive(BorshSerialize, BorshDeserialize, PartialEq)]
pub enum AccountDiscriminant {
User,
Admin,
}
要在 Rust 中实现鉴别器检查,请验证反序列化的账户数据的鉴别器是否与预期值匹配
if user.discriminant != AccountDiscriminant::User {
return Err(ProgramError::InvalidAccountData.into());
}
在 Anchor 中,程序账户类型自动实现了 Discriminator
trait,这为一个类型创建了一个 8 字节唯一标识符。
使用 Anchor 的 Account<'info, T>
类型在反序列化账户数据时自动检查账户的鉴别器。
##[derive(Accounts)]
pub struct UpdateUserData<'info> {
pub user: Signer<'info>,
// 账户类型自动检查 UserData 账户的鉴别器
pub user_data: Account<'info, UserData>,
}
要生成 CPI,目标程序必须作为账户传入调用指令。这意味着任何目标程序都可以传入指令。你的程序应检查不正确或意外的程序。
通过比较传入程序的公钥与你预期的程序进行程序检查。
pub fn cpi_secure(ctx: Context<Cpi>, amount: u64) -> Result<()> {
if &spl_token::ID != ctx.accounts.token_program.key {
return Err(ProgramError::IncorrectProgramId);
}
...
// CPI 逻辑在这里
}
如果程序是用 Anchor 编写的,则可能有一个公开可用的 CPI 模块。这使得从另一个 Anchor 程序调用程序既简单又安全。Anchor CPI 模块自动检查传入程序的地址是否与模块中存储的程序地址匹配。
使用 Anchor 的最佳实践是始终使用 Program<'info, T>
,这将检查账户是否可执行且为给定程序。例如:
use anchor_spl::token::Token;
##[derive(Accounts)]
pub struct InitializeExchange<'info> {
// ...
pub token_program: Program<'info, Token>,
// ...
}
create_program_address
函数在不搜索 canonical bump
的情况下推导 PDA。这意味着可能存在多个有效的 bump,所有这些 bump 都将生成不同的地址。
使用 find_program_address
确保使用最高有效 bump 或 canonical bump 进行推导,从而以确定性的方法找到给定特定种子的地址。
推荐工作流:
find_program_address
推导 PDA。这会生成包含标识 bump 的 PDA。下一步是在账户中存储生成的 bump 和程序数据。create_program_address
及步骤 1 中存储的 bump。在初始化时,你可以使用 Anchor 的 seeds
和 bump
约束确保账户验证结构中的 PDA 推导始终使用 canonical bump
。
当验证 PDA 的地址时,Anchor 允许你用 bump = <some_bump>
约束指定一个 bump。
pub fn _initialize_exchange(
ctx: Context<InitializeExchange>,
) -> Result<()> {
// ...
// 存储生成的 canonical bump
let escrow = &mut ctx.accounts.escrow;
escrow.bump = ctx.bumps.escrow;
// ...
Ok(())
}
pub fn _update_exchange(
ctx: Context<UpdateExchange>,
) -> Result<()> {
// 在上下文中确保地址的正确性
}
pub struct InitializeExchange<'info> {
#[account(mut)]
pub side_a: Signer<'info>,
#[account(
init,
payer = side_a,
space = 8 + Escrow::LEN,
seeds = [b"escrow"], // 在这里指定所需种子
bump // 生成 canonical bump
)]
pub escrow: Account<'info, Escrow>,
// ...
}
pub struct UpdateExchange<'info> {
#[account(
mut,
seeds = [b"escrow"], // 在这里指定所需种子
escrow.bump // 检查与已存储的 bump 是否相符
)]
pub escrow: Account<'info, Escrow>,
}
不当关闭账户
为重新初始化/复生攻击创造了机会。
当账户不再免租时,Solana 运行时会 垃圾收集账户
。关闭账户涉及将存储在该账户中的 lamports 转移到你所选择的另一个账户以进行租金豁免。
手动关闭账户的推荐工作流是使用 Anchor 的逻辑(即检查关闭 参考)
你可以使用 Anchor 的 #[account(close = <address_to_send_lamports>)]
约束安全地关闭账户。关闭参数将 (关闭参考):
<address_to_send_lamports>
系统程序
例如:
##[derive(Accounts)]
pub struct CancelExchange<'info> {
#[account(mut)]
pub side_a: Signer<'info>,
#[account(
mut,
close = side_a, // 这是 <address_to_send_lamports> (即租金接收者)
seeds = [b"escrow"],
escrow.bump
)]
pub escrow: Account<'info, Escrow>,
}
- 原文链接: github.com/Ackee-Blockch...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!