Solana 审计 第1课 - 高级 Anchor

这篇文章详细介绍了Anchor框架中高级功能的实现,包括属性宏和派生宏的使用,账户类型的介绍,以及账户约束检查的实现等。文章提供了大量的示例代码,深入探讨了如何在Solana程序中使用这些高级特性,以便开发者能够更好地理解和利用Anchor进行Solana智能合约的开发。

课时 1 - 高级 Anchor

目录

属性宏 #[program]

文档

程序指令入口调度

  1. entry 函数。
  2. try_entry 函数。
  3. PROGRAM_ID 以及足够的长度用于指令 DISCRIMINATOR 检查。
  4. 剥离前 8 字节作为指令 DISCRIMINATOR
  5. 根据指令 DISCRIMINATOR 进行 Dispatch 指令。
  6. 调用账户反序列化并检查指定的约束条件。

/// 1.
entrypoint!(entry);

pub fn entry<'info>(
    program_id: &Pubkey,
    accounts: &'info [AccountInfo<'info>],
    data: &[u8],
) -> anchor_lang::solana_program::entrypoint::ProgramResult {
    try_entry(program_id, accounts, data)
        .map_err(|e| {
            e.log();
            e.into()
        })
}

/// 2.
fn try_entry<'info>(
    program_id: &Pubkey,
    accounts: &'info [AccountInfo<'info>],
    data: &[u8],
) -> anchor_lang::Result<()> {
    /// 3.
    if *program_id != ID {
        return Err(anchor_lang::error::ErrorCode::DeclaredProgramIdMismatch.into());
    }
    /// 3.
    if data.len() < 8 {
        return Err(anchor_lang::error::ErrorCode::InstructionMissing.into());
    }
    dispatch(program_id, accounts, data)
}

fn dispatch<'info>(
    program_id: &Pubkey,
    accounts: &'info [AccountInfo<'info>],
    data: &[u8],
) -> anchor_lang::Result<()> {
    /// 4.
    let mut ix_data: &[u8] = data;
    let sighash: [u8; 8] = {
        let mut sighash: [u8; 8] = [0; 8];
        sighash.copy_from_slice(&ix_data[..8]);
        ix_data = &ix_data[8..];
        sighash
    };
    use anchor_lang::Discriminator;
    /// 5.
    match sighash {
        instruction::Initialize::DISCRIMINATOR => {
            __private::__global::initialize(program_id, accounts, ix_data)
        }
        anchor_lang::idl::IDL_IX_TAG_LE => {
            __private::__idl::__idl_dispatch(program_id, accounts, &ix_data)
        }
        anchor_lang::event::EVENT_IX_TAG_LE => {
            Err(anchor_lang::error::ErrorCode::EventInstructionStub.into())
        }
        _ => Err(anchor_lang::error::ErrorCode::InstructionFallbackNotFound.into()),
    }
}

mod __private {
    use super::*;
    /// __idl 模块定义了注入的 Anchor IDL 指令的处理程序。
    pub mod __idl { ... }
    /// __global 模块定义了全局指令的包装处理程序。
    pub mod __global {
        use super::*;
        #[inline(never)]
        pub fn initialize<'info>(
            __program_id: &Pubkey,
            __accounts: &'info [AccountInfo<'info>],
            __ix_data: &[u8],
        ) -> anchor_lang::Result<()> {
            ::solana_program::log::sol_log("指令: 初始化");

            ...

            /// 6.
            let mut __accounts = Initialize::try_accounts(
                __program_id,
                &mut __remaining_accounts,
                __ix_data,
                &mut __bumps,
                &mut __reallocs,
            )?;

            ...

        }
    }
}

派生宏 Accounts

文档

在给定的结构上实现一个账户反序列化器。可以通过账户约束提供进一步的功能。

  1. __private::__global::initialize 函数调用。
  2. 调用给定账户类型的 try_accounts 函数实现。
    • 对于数据账户,执行反序列化
  3. 如果指定,执行账户约束检查(例如,如果指定了 init -> 初始化该账户)。
  4. 生成 CPI 结构(在下面的代码片段中未出现,可以在 anchor_solana-expanded.rs 中看到)。

...

impl<'info> anchor_lang::Accounts<'info, InitializeBumps> for Initialize<'info>
where
    'info: 'info,
{
    /// 1.
    #[inline(never)]
    fn try_accounts(
        __program_id: &anchor_lang::solana_program::pubkey::Pubkey,
        __accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo<
            'info,
        >],
        __ix_data: &[u8],
        __bumps: &mut InitializeBumps,
        __reallocs: &mut std::collections::BTreeSet<
            anchor_lang::solana_program::pubkey::Pubkey,
        >,
    ) -> anchor_lang::Result<Self> {
        /// 2.
        let signer: Signer = anchor_lang::Accounts::try_accounts(
                __program_id,
                __accounts,
                __ix_data,
                __bumps,
                __reallocs,
            )
            .map_err(|e| e.with_account_name("signer"))?;

        /// 2.
        let system_program: anchor_lang::accounts::program::Program<System> = anchor_lang::Accounts::try_accounts(
                __program_id,
                __accounts,
                __ix_data,
                __bumps,
                __reallocs,
            )
            .map_err(|e| e.with_account_name("system_program"))?;

        /// 3. 如果指定了账户约束

        Ok(Initialize {
            signer,
            system_program,
        })
    }
}

...

属性宏 #[account]

文档

表示 Solana 账户的数据结构的属性。为给定的特征生成实现

  • AccountSerialize
  • AccountDeserialize
  • AnchorSerialize
  • AnchorDeserialize
  • Clone
  • Discriminator
  • Owner

/// 作为参考 - 原始定义
/// #[account]
/// pub struct DataAccount {
///     pub authority: Pubkey,
///     pub counter: u64,
/// }

/// 账户序列化的实现
impl anchor_lang::AccountSerialize for DataAccount {
    fn try_serialize<W: std::io::Write>(
        &self,
        writer: &mut W,
    ) -> anchor_lang::Result<()> {
        if writer.write_all(&[85, 240, 182, 158, 76, 7, 18, 233]).is_err() {
            return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
        }
        if AnchorSerialize::serialize(self, writer).is_err() {
            return Err(anchor_lang::error::ErrorCode::AccountDidNotSerialize.into());
        }
        Ok(())
    }
}

/// 账户反序列化的实现
impl anchor_lang::AccountDeserialize for DataAccount {
    fn try_deserialize(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
        if buf.len() < [85, 240, 182, 158, 76, 7, 18, 233].len() {
            return Err(
                anchor_lang::error::ErrorCode::AccountDiscriminatorNotFound.into(),
            );
        }
        let given_disc = &buf[..8];
        if &[85, 240, 182, 158, 76, 7, 18, 233] != given_disc {
            /// 错误信息过长 -> 删除
            return Err( ... )
        }
        Self::try_deserialize_unchecked(buf)
    }
    fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
        let mut data: &[u8] = &buf[8..];
        AnchorDeserialize::deserialize(&mut data)
            .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize.into())
    }
}

/// Discriminator 的实现
impl anchor_lang::Discriminator for DataAccount {
    const DISCRIMINATOR: [u8; 8] = [85, 240, 182, 158, 76, 7, 18, 233];
}

/// DataAccount 的实现
impl anchor_lang::Owner for DataAccount {
    fn owner() -> Pubkey {
        crate::ID
    }
}

Anchor 的内部 Discriminator

文档

考虑一个管理两种类型账户的程序,账户 A 和账户 B。这两个账户都由相同的程序拥有,并且具有相同的字段。现在,假设你有一个指令叫做 foo,只能在账户 A 上操作。

然而,用户错误地将账户 B 作为参数传递给 foo 指令。由于账户 B 与账户 A 具有相同的所有者和相同的字段,程序如何检测到这个错误并抛出错误?

这就是 discriminator 的作用。它唯一标识账户的类型。即使账户 A 和账户 B 在结构上是相同的并且共享相同的所有者,它们也具有不同的 discriminator。

[!注意] DISCRIMINATOR 的长度是 8 字节。

零拷贝反序列化

文档

[!警告] 零拷贝反序列化是一个实验性特性。建议仅在必要时使用,即当你有无法在不触及栈或堆限制的情况下进行 Borsh 反序列化的极大账户时。

要启用零拷贝反序列化,可以将 zero_copy 参数传递给宏,如下所示:

##[account(zero_copy)]

[!注意] 除了更高效外,此提供的最显著好处是能够定义大于最大栈或堆大小的账户类型。当使用 borsh 时,账户必须被拷贝并反序列化到新的数据结构中,因此受到 BPF 虚拟机 imposed 的栈和堆限制的约束。使用零拷贝反序列化时,账户的后备 RefCell<&mut [u8]> 中的所有字节只是被重新解释为数据结构的引用。不需要分配或拷贝。因此有能力绕过栈和堆的限制。

Anchor 扩展

可以查看所有扩展的 Anchor 宏。

[!提示]

  • 使用 anchor expand 命令查看在 Solana 程序中扩展的所有宏。

账户类型

账户

来源

  • 包装在 AccountInfo 周围,验证程序所有权并将底层数据反序列化为 Rust 类型。
##[derive(Accounts)]
pub struct Context&lt;'info> {
    pub account: Account&lt;'info, CustomAccount>,
}

AccountInfo

来源

[!谨慎] 此类型未执行任何验证检查。

  • 原始 AccountInfo。
##[derive(Accounts)]
pub struct Context&lt;'info> {
    /// CHECK: AccountInfo 是未检查的账户
    pub account: AccountInfo&lt;'info>,
}

AccountLoader

来源

  • 促进按需零拷贝反序列化的类型。

[!注意] 注意,以这种方式使用账户明显不同于使用例如账户。即,必须调用

  • load_init 在初始化账户后(这将忽略用户指令代码中仅在用户指令代码后添加的缺失账户 discriminator)
  • load 当账户不可变时
  • load_mut 当账户是可变的
##[derive(Accounts)]
pub struct Context&lt;'info> {
    pub account: AccountLoader&lt;'info, CustomAccount>,
}

Boxed

来源

  • 盒子类型以节省堆栈空间。
  • 有时账户对于堆栈来说太大,导致堆栈违规。
##[derive(Accounts)]
pub struct Context&lt;'info> {
    pub account: Box&lt;Account&lt;'info, CustomAccount>>,
}

接口

来源

  • 验证账户是给定一组程序之一的类型

[!注意] 程序 ID 验证来自程序 ID 集。

[!提示] TokenInterface 的示例

##[derive(Accounts)]
pub struct Context&lt;'info> {
    pub program: Interface&lt;'info, TokenInterface>,
}

InterfaceAccount

来源

  • 包装在 AccountInfo 周围,验证程序所有权(来自集合)并将底层数据反序列化为 Rust 类型。

[!注意] 所有者验证来自程序 ID 集合。

[!提示] Mint Account 的示例

##[derive(Accounts)]
pub struct Context&lt;'info> {
    pub account: InterfaceAccount&lt;'info, TokenAccount>,
}

Option

来源

  • optional 账户的 Option 类型。
    ##[derive(Accounts)]
    pub struct Context&lt;'info> {
    pub account: Option&lt;Account&lt;'info, CustomAccount>>,
    }

Program

来源

  • 验证账户是给定程序的类型。
    ##[derive(Accounts)]
    pub struct Context&lt;'info> {
    pub program: Program&lt;'info, System>,
    }

Signer

来源

  • 验证账户签署了交易。没有其他所有权或类型检查。
    ##[derive(Accounts)]
    pub struct Context&lt;'info> {
    pub account: Signer&lt;'info>,
    }

SystemAccount

来源

  • 验证账户由系统程序拥有的类型。
    ##[derive(Accounts)]
    pub struct Context&lt;'info> {
    pub account: SystemAccount&lt;'info>,
    }

Sysvar

来源

  • 验证账户是 sysvar 类型并反序列化它。
    ##[derive(Accounts)]
    pub struct Context&lt;'info> {
    pub sysvar: Sysvar&lt;'info, Clock>,
    }

UncheckedAccount

来源

[!谨慎] 此类型未执行任何验证检查。

  • 明确为 AccountInfo 类型的包装,以强调未执行任何检查。
##[derive(Accounts)]
pub struct Context&lt;'info> {
    /// CHECK: 对 AccountInfo 类型的显式包装以强调未执行任何检查
    pub account: UncheckedAccount&lt;'info>,
}

账户约束

普通

文档

mut
  • 检查给定账户是否可变。
  • 通过 @ 支持自定义错误。
##[account(mut)]
init, space, payer
  • 通过对系统程序的 CPI 创建账户并初始化它(设置其账户 discriminator)。
  • 将账户标记为可变,且与 mut 是互斥的。
  • 除非通过 rent_exempt = skip 跳过,否则使账户免于租金。
##[account(
    init,
    payer = &lt;target_account>,
    space = &lt;num_bytes>
)]
init_if_needed
  • 功能与 init 约束完全相同,但仅在账户尚不存在时运行。
  • 如果账户已存在,它仍然检查给定的 init 约束是否正确,例如账户是否具有预期的空间数量,以及如果它是 PDA,正确的种子等。

[!谨慎] 你需要确保自己妥善防止再初始化攻击,即如果账户已被初始化,你必须确保账户的字段不会重置为初始状态。

##[account(
    init_if_needed,
    payer = &lt;target_account>,
    space = &lt;num_bytes>
)]
seeds
  • 检查给定账户是由当前执行程序、种子和如果提供的 bump 导出的 PDA。如果不提供,anchor 使用规范 bump。
  • 添加 seeds::program = <expr> 以从与当前执行的程序不同的程序导出 PDA。
##[account(
    seeds = &lt;seeds>,
    bump = &lt;expr>,
    seeds::program = &lt;expr>
)]
has_one
  • 检查账户中的 target_account 字段是否与 Accounts 结构中的 target_account 字段的密钥匹配。
  • 通过 @ 支持自定义错误。
##[account(
    has_one = &lt;target_account> @ &lt;custom_error>
)]
address
  • 检查账户密钥与 pubkey 是否匹配。
  • 通过 @ 支持自定义错误。
##[account(
    address = &lt;expr> @ &lt;custom_error>
)]
owner
  • 检查账户所有者是否与 expr 匹配。
  • 通过 @ 支持自定义错误。
##[account(
    owner = &lt;expr> @ &lt;custom_error>
)]
executable
  • 检查账户是否可执行(即账户是否是程序)。
  • 你可能想使用 Program 类型。
##[account(executable)]
rent_exempt
  • 通过 = enforce 强制执行租金豁免。
  • 通过 = skip 跳过通常通过其他约束进行的租金豁免检查,例如在与零约束一起使用时。
##[account(rent_exempt = skip)]

##[account(rent_exempt = enforce)]
zero
  • 检查账户 DISCRIMINATOR 是否为零。
  • 强制执行租金豁免,除非通过 rent_exempt = skip 跳过。

[!提示] 如果你希望在以前的指令中创建账户,然后在你的指令中初始化它而不是使用 init,则使用此约束。这对于大于 10 Kibibyte 的账户是必要的,因为这些账户无法通过 CPI 创建(这也是 init 的功能)。

##[account(zero)]
close
  • 通过以下方式关闭账户:

    • 将 lamports 发送到指定的账户
    • 将所有权分配给系统程序
    • 重置账户的数据
  • 需要账户具备 mut。

##[account(close = &lt;target_account>)]
constraint
  • 检查给定表达式评估为真的约束。
  • 当没有其他约束适合你的用例时使用此约束。
##[account(constraint = &lt;expr> @ &lt;custom_error>)]
realloc
  • 账户必须标记为可变,且适用于 Account 或 AccountLoader 类型。
  • 数据长度变化是附加的 -> lamports 从 realloc::payer 转移。
  • 数据长度变化是减法的 -> lamports 从数据账户转移到 realloc::payer。
  • 需要使用 realloc::zero 约束来确定是否在重新分配后新内存应被零初始化。阅读 AccountInfo::realloc 函数的文档以理解提供 true/false 到该标志时的计算单位的注意事项。

[!警告] 由于缺乏原生的运行时检查,以防止重新分配超过 MAX_PERMITTED_DATA_INCREASE 限制(这可能无意中导致账户数据覆盖其他账户),因此不推荐手动使用 AccountInfo::realloc,而应使用 realloc 约束组。该约束组还通过在单个 ix 内检查和限制重复账户重新分配来确保账户重新分配的幂等性。

##[account(
    realloc = &lt;space>,
    realloc::payer = &lt;target>,
    realloc::zero = &lt;bool>
)]

Solana 程序库

文档

代币账户
  • 可用于检查或与 init 一起创建具有给定铸造地址和权限的代币账户。
  • 当用作检查时,可以只指定约束的一部分。
use anchor_spl::{mint, token::{TokenAccount, Mint, Token}};
...

##[account(
    init,
    payer = payer,
    token::mint = mint,
    token::authority = payer,
)]
pub token: Account&lt;'info, TokenAccount>,
##[account(address = mint::USDC)]
pub mint: Account&lt;'info, Mint>,
##[account(mut)]
pub payer: Signer&lt;'info>,
pub token_program: Program&lt;'info, Token>,
pub system_program: Program&lt;'info, System>
铸造
  • 可用于检查或与 init 一起创建具有给定铸造小数位和铸造权限的铸造账户。
  • 冻结权限在与 init 一起使用时是可选的。
  • 当用作检查时,可以只指定约束的一部分。
use anchor_spl::token::{Mint, Token};
...

##[account(
    init,
    payer = payer,
    mint::decimals = 9,
    mint::authority = payer,
)]
pub mint_one: Account&lt;'info, Mint>,
##[account(
    init,
    payer = payer,
    mint::decimals = 9,
    mint::authority = payer,
    mint::freeze_authority = payer
)]
pub mint_two: Account&lt;'info, Mint>,
##[account(mut)]
pub payer: Signer&lt;'info>,
pub token_program: Program&lt;'info, Token>,
pub system_program: Program&lt;'info, System>
关联代币账户
  • 可作为独立检查或与 init 一起创建具有给定铸造地址和权限的关联代币账户。
use anchor_spl::{
    associated_token::AssociatedToken,
    mint,
    token::{TokenAccount, Mint, Token}
};
...

##[account(
    init,
    payer = payer,
    associated_token::mint = mint,
    associated_token::authority = payer,
)]
pub token: Account&lt;'info, TokenAccount>,
##[account(
    associated_token::mint = mint,
    associated_token::authority = payer,
)]
pub second_token: Account&lt;'info, TokenAccount>,
##[account(address = mint::USDC)]
pub mint: Account&lt;'info, Mint>,
##[account(mut)]
pub payer: Signer&lt;'info>,
pub token_program: Program&lt;'info, Token>,
pub associated_token_program: Program&lt;'info, AssociatedToken>,
pub system_program: Program&lt;'info, System>
代币程序覆盖

可以选择重写 token_program。

##[account(
    mint::token_program = token_program,
)]
pub mint_account: InterfaceAccount&lt;'info, Mint>,
##[account(
    token::token_program = token_program,
)]
pub token_account: InterfaceAccount&lt;'info, TokenAccount>,
pub token_program: Interface&lt;'info, TokenInterface>,

账户空间

空间参考

文档

类型 大小(字节) 描述
bool 1 只需要 1 位,但仍使用 1 字节
u8/i8 1
u16/i16 2
u32/i32 4
u64/i64 8
u128/i128 16
[T; amount] space(T) * amount 例如 space([u16; 32]) = 2 * 32 = 64
Pubkey 32
Vec&lt;T> 4 + (space(T) * amount)
String 4 + 字符串长度(字节)
Option&lt;T> 1 + space(T)
Enum 1 + 最大变量大小 例如 Enum { A, B { val: u8 }, C { val: u16 } } -> 1 + space(u16) = 3
f32 4
f64 8

调整程序空间

[!提示]

  • 如果要增加/减少数据账户的大小,请使用 realloc 约束。

  • 如果由于部署时空间不足而无法部署程序(“账户数据对于指令太小”),请使用 solana program extend &lt;PROGRAM_ID> &lt;MORE_BYTES> CLI 命令。

  • 原文链接: github.com/Ackee-Blockch...

  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ackee-Blockchain
Ackee-Blockchain
江湖只有他的大名,没有他的介绍。