一文详细梳理Bank合约业务逻辑

文章背景:

上篇文章,我们使用 Anchor 工程化环境,从初始化项目、编译、部署、测试各个环节演示了一个真实的 solana 链上程序的开发流程。这篇文章,我们从语法和业务的角度来梳理下我们实现的 Bank 合约的源码。

基于对源码和业务的的理解,我们后续可以扩展这个合约,设置一些更加复杂的功能。

Bank 合约源码:

rust 复制代码
use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;

declare_id!("ditw8dH7D93kotkJgokM6WLbJHNdrbK9fJfLR74NJ7h");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        msg!("Initializing bank contract");
        let bank = &mut ctx.accounts.bank;
        bank.owner = ctx.accounts.owner.key();
        bank.total_balance = 0;
        Ok(())
    }

    pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
        const MIN_DEPOSIT: u64 = 10_000_000; // 0.01 SOL
        msg!(
            "Processing deposit of {} lamports from user: {}",
            amount,
            ctx.accounts.user.key()
        );
        require!(amount >= MIN_DEPOSIT, BankError::DepositTooSmall);

        let transfer_instruction = system_instruction::transfer(
            &ctx.accounts.user.key(),
            &ctx.accounts.bank.key(),
            amount,
        );

        anchor_lang::solana_program::program::invoke(
            &transfer_instruction,
            &[
                ctx.accounts.user.to_account_info(),
                ctx.accounts.bank.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        let user_account = &mut ctx.accounts.user_account;
        let old_balance = user_account.balance;
        user_account.balance = user_account.balance.checked_add(amount).unwrap();

        let bank = &mut ctx.accounts.bank;
        bank.total_balance = bank.total_balance.checked_add(amount).unwrap();
        msg!(
            "Deposit successful. User balance: {} -> {}, Bank total: {}",
            old_balance,
            user_account.balance,
            bank.total_balance
        );

        Ok(())
    }

    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        let user_account = &mut ctx.accounts.user_account;
        msg!(
            "Processing withdrawal of {} lamports for user: {}",
            amount,
            ctx.accounts.user.key()
        );
        require!(user_account.balance >= amount, BankError::InsufficientFunds);

        let old_balance = user_account.balance;
        let old_bank_balance = ctx.accounts.bank.total_balance;

        **ctx
            .accounts
            .bank
            .to_account_info()
            .try_borrow_mut_lamports()? -= amount;
        **ctx
            .accounts
            .user
            .to_account_info()
            .try_borrow_mut_lamports()? += amount;

        user_account.balance = user_account.balance.checked_sub(amount).unwrap();

        let bank = &mut ctx.accounts.bank;
        bank.total_balance = bank.total_balance.checked_sub(amount).unwrap();
        msg!(
            "Withdrawal successful. User balance: {} -> {}, Bank total: {} -> {}",
            old_balance,
            user_account.balance,
            old_bank_balance,
            bank.total_balance
        );

        Ok(())
    }

    pub fn get_balance(ctx: Context<GetBalance>) -> Result<u64> {
        let balance = ctx.accounts.user_account.balance;
        msg!(
            "Queried balance for user {}: {} lamports",
            ctx.accounts.user.key(),
            balance
        );
        Ok(balance)
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init_if_needed,
        payer = owner,
        space = 8 + 32 + 8,
        seeds = [b"bank"],
        bump
    )]
    pub bank: Account<'info, Bank>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut, seeds = [b"bank"], bump)]
    pub bank: Account<'info, Bank>,
    #[account(
        init_if_needed,
        payer = user,
        space = 8 + 8,
        seeds = [b"user", user.key().as_ref()],
        bump
    )]
    pub user_account: Account<'info, UserAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(mut, seeds = [b"bank"], bump)]
    pub bank: Account<'info, Bank>,
    #[account(mut, seeds = [b"user", user.key().as_ref()], bump)]
    pub user_account: Account<'info, UserAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct GetBalance<'info> {
    #[account(seeds = [b"user", user.key().as_ref()], bump)]
    pub user_account: Account<'info, UserAccount>,
    pub user: Signer<'info>,
}

#[account]
pub struct Bank {
    pub owner: Pubkey,
    pub total_balance: u64,
}

#[account]
pub struct UserAccount {
    pub balance: u64,
}

#[error_code]
pub enum BankError {
    #[msg("Deposit amount must be at least 0.01 SOL")]
    DepositTooSmall,
    #[msg("Insufficient funds for withdrawal")]
    InsufficientFunds,
}

程序的核心功能:

初始化银行( initialize

  • 创建一个银行账户(PDA,种子 b"bank"),设置 owner 并初始化总余额 total_balance = 0
  • owner 支付账户初始化费用(rent)。

存款( deposit

  • 用户存入 SOL(最少 0.01 SOL,即 10_000_000 lamports)。
  • 使用 Solana 系统指令 system_instruction::transfer 完成转账。
  • 更新用户的 UserAccount 余额和银行总余额。

取款( withdraw

  • 用户提取 SOL,需确保余额足够。
  • 直接修改账户的 lamports(无需 invoke,更高效)。
  • 更新用户余额和银行总余额。

查询余额( get_balance

  • 返回用户的 UserAccount.balance

程序的核心业务并不复杂,如果说对于新手有难度和门槛的应该是对账户模型的理解,下面我们按照指令和账户约束的层面,从技术层面和业务层面来分析下源码:

initialize 指令:

rust 复制代码
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    msg!("Initializing bank contract");
    let bank = &mut ctx.accounts.bank;
    bank.owner = ctx.accounts.owner.key();
    bank.total_balance = 0;
    Ok(())
}

1. 函数签名分析

rust 复制代码
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {

语法层面

  • pub fn initialize:定义一个公开的(pub)函数 initialize
  • ctx: Context<Initialize>:接收一个 Context 参数,泛型类型是 Initialize(表示该函数只能由 Initialize 结构体定义的账户调用)。
  • -> Result<()>:返回 Anchor 的 Result 类型,() 表示无具体返回值(仅返回成功/错误状态)。

业务层面

  • 该函数用于 初始化银行合约 ,通常由合约的部署者(owner)调用。
  • Context<Initialize> 确保调用时必须传入符合 Initialize 结构体定义的账户(如 bankowner 等)。

2. 日志输出

arduino 复制代码
msg!("Initializing bank contract");

语法层面

  • msg! 是 Anchor 提供的宏,用于在 Solana 链上打印日志(类似 println!)。
  • 日志内容会记录在交易日志中,便于调试和监控。

业务层面

  • 用于调试,标记合约初始化开始执行。

3. 获取 Bank 账户的可变引用

ini 复制代码
let bank = &mut ctx.accounts.bank;

语法层面

  • ctx.accounts.bank:从 Context 中获取 bank 账户(定义在 Initialize 结构体)。
  • &mut:获取可变引用(因为要修改 bank 的数据)。

业务层面

  • 这里操作的是 银行的主账户Bank 结构体实例),后续会设置 ownertotal_balance

4. 设置 Bank 的 owner

ini 复制代码
bank.owner = ctx.accounts.owner.key();

语法层面

  • bank.ownerBank 结构体的 owner 字段(类型是 Pubkey)。
  • ctx.accounts.owner.key():获取 owner 账户的公钥(Signer 类型的账户)。

业务层面

  • Bankowner 设置为调用者(ctx.accounts.owner),表示该银行合约的管理者。
  • 关键点
    • owner 在后续可用于权限控制(如仅允许 owner 调用某些管理函数)。

5. 初始化 Bank 的总余额

ini 复制代码
bank.total_balance = 0;

语法层面

  • bank.total_balanceBank 结构体的 total_balance 字段(类型是 u64)。
  • = 0:初始化为 0(表示银行初始资金为 0)。

业务层面

  • 银行刚创建时,总存款(total_balance)应为 0
  • 后续 deposit/withdraw 会更新这个值。

6. 返回成功

scss 复制代码
Ok(())

语法层面

  • Ok(()):返回 Result::Ok,表示函数执行成功,无返回值。
  • 如果出错,可以返回 Err(BankError::SomeError)

业务层面

  • 表示初始化成功,合约可以正常使用。

Initialize 账户约束:

rust 复制代码
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init_if_needed,
        payer = owner,
        space = 8 + 32 + 8,
        seeds = [b"bank"],
        bump
    )]
    pub bank: Account<'info, Bank>,
    #[account(mut)]
    pub owner: Signer<'info>,
    pub system_program: Program<'info, System>,
}

结构体定义

rust 复制代码
#[derive(Accounts)]
pub struct Initialize<'info> {

语法层面:

  • #[derive(Accounts)]:宏标记,表示该结构体是 Anchor 的 账户验证容器,用于定义指令的账户约束。
  • <'info>:生命周期泛型,表示这些账户引用在交易执行期间有效。

语法层面:

  • 定义了 initialize 指令所需的账户集合,Anchor 会自动验证传入的账户是否符合这些约束。

银行主账户 bank

rust 复制代码
#[account(
    init_if_needed,
    payer = owner,
    space = 8 + 32 + 8,
    seeds = [b"bank"],
    bump
)]
pub bank: Account<'info, Bank>,

语法层面:

  • #[account(...)]:属性宏,定义账户的初始化规则和安全约束。
  • init_if_needed:如果账户未初始化,则自动初始化;否则跳过(防重复初始化)。
  • payer = owner:初始化费用(rent)由 owner 账户支付。
  • space = 8 + 32 + 8:分配存储空间:
    • 8:Anchor 的账户标识头。
    • 32Bank.ownerPubkey 类型,固定 32 字节)。
    • 8Bank.total_balanceu64 类型,固定 8 字节)。
  • seeds = [b"bank"]:定义 PDA(Program Derived Address)的种子,此处为静态字符串 "bank"
  • bump:自动计算 PDA 的 bump 值(避免地址冲突)。

业务层面:

  • 创建或复用银行的全局状态账户,存储 ownertotal_balance
  • PDA 确保账户地址唯一性(通过 program_id + seeds 派生)。

调用者账户 owner

rust 复制代码
#[account(mut)]
pub owner: Signer<'info>,

语法层面:

  • #[account(mut)]:标记该账户为 可变(因为要支付 rent,需修改 lamports)。
  • Signer<'info>:要求 owner 必须对当前交易签名。

业务层面:

  • 调用者必须是真人钱包(具备签名能力)。
  • 支付银行账户的初始化费用(rent)。

系统程序 system_program

rust 复制代码
pub system_program: Program<'info, System>,

语法层面:

  • Program<'info, System>:显式声明依赖 Solana 系统程序。
  • 无需 mut,因为只读访问。

业务层面:

  • 用于执行账户初始化(init_if_needed 内部会调用系统程序)。
  • Anchor 要求所有涉及账户创建的操作必须传入 system_program

关键安全机制总结

属性/字段 语法作用 业务意义
init_if_needed 按需初始化账户 防止重复初始化,节省 rent 费用
payer = owner 指定支付者 调用者承担初始化成本
seeds = [b"bank"] PDA 种子 确保银行账户地址唯一且由程序控制
Signer 强制签名验证 防止匿名调用
system_program 显式依赖系统程序 合规性要求(账户创建必须通过系统程序)

为什么需要这些约束?

防重复初始化 :通过 init_if_needed + owner 检查,确保银行账户只被初始化一次。

经济模型payer = owner 让调用者支付存储费用(防止滥用)。

权限控制Signer 确保只有合法用户能调用,后续可通过 bank.owner 实现更复杂的权限管理。

确定性地址 : PDA 保证 bank 账户地址可预测(基于 seeds),避免地址冲突。

deposit 指令:

rust 复制代码
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
    const MIN_DEPOSIT: u64 = 10_000_000; // 0.01 SOL
    msg!(
        "Processing deposit of {} lamports from user: {}",
        amount,
        ctx.accounts.user.key()
    );
    require!(amount >= MIN_DEPOSIT, BankError::DepositTooSmall);

    let transfer_instruction = system_instruction::transfer(
        &ctx.accounts.user.key(),
        &ctx.accounts.bank.key(),
        amount,
    );

    anchor_lang::solana_program::program::invoke(
        &transfer_instruction,
        &[
            ctx.accounts.user.to_account_info(),
            ctx.accounts.bank.to_account_info(),
            ctx.accounts.system_program.to_account_info(),
        ],
    )?;

    let user_account = &mut ctx.accounts.user_account;
    let old_balance = user_account.balance;
    user_account.balance = user_account.balance.checked_add(amount).unwrap();

    let bank = &mut ctx.accounts.bank;
    bank.total_balance = bank.total_balance.checked_add(amount).unwrap();
    msg!(
        "Deposit successful. User balance: {} -> {}, Bank total: {}",
        old_balance,
        user_account.balance,
        bank.total_balance
    );

    Ok(())
}

函数签名

rust 复制代码
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {

语法

  • pub fn deposit:定义公开的存款函数
  • ctx: Context<Deposit>接收Deposit账户上下文
  • amount: u64:接收存款金额参数 (以lamports为单位)
  • -> Result<()>返回Anchor标准结果类型
  • 业务
    • 这是用户向银行合约存款的入口函数
    • 需要传入正确的账户结构和存款金额

最小存款常量

ini 复制代码
const MIN_DEPOSIT: u64 = 10_000_000; // 0.01 SOL

语法

  • 定义编译时常量
  • 使用下划线提高可读性(10_000_000 = 10000000)

业务

  • 设置最小存款金额为0.01 SOL
  • 防止小额存款造成的垃圾交易

日志记录

css 复制代码
msg!(
    "Processing deposit of {} lamports from user: {}",
    amount,
    ctx.accounts.user.key()
);

语法

  • 使用msg!宏记录日志
  • 格式化字符串包含金额和用户地址

业务

  • 记录交易开始执行的日志
  • 便于调试和链上监控

金额验证

php 复制代码
require!(amount >= MIN_DEPOSIT, BankError::DepositTooSmall);

语法

  • require!宏验证条件
  • 不满足时返回自定义错误

业务

  • 确保存款金额≥最小值
  • 防止滥用合约功能

创建转账指令

css 复制代码
let transfer_instruction = system_instruction::transfer(
    &ctx.accounts.user.key(),
    &ctx.accounts.bank.key(),
    amount,
);

语法

  • 调用Solana系统程序的transfer函数
  • 生成从用户到银行的转账指令

业务

  • 准备SOL转账的底层指令
  • 指定转出账户、转入账户和金额

执行转账

css 复制代码
anchor_lang::solana_program::program::invoke(
    &transfer_instruction,
    &[
        ctx.accounts.user.to_account_info(),
        ctx.accounts.bank.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
    ],
)?;

语法

  • invoke执行跨程序调用
  • 传入指令和所需账户

业务

  • 实际执行SOL转账
  • 需要用户、银行账户和系统程序的参与

更新用户余额

ini 复制代码
let user_account = &mut ctx.accounts.user_account;
let old_balance = user_account.balance;
user_account.balance = user_account.balance.checked_add(amount).unwrap();

语法

  • 获取用户账户的可变引用
  • 使用checked_add防止整数溢出
  • 记录旧余额用于日志

业务

  • 在链上记录用户的新余额
  • 确保余额计算安全

更新银行总余额

ini 复制代码
let bank = &mut ctx.accounts.bank;
bank.total_balance = bank.total_balance.checked_add(amount).unwrap();
  • 语法
    • 获取银行账户的可变引用
    • 同样使用安全数学运算
  • 业务
    • 维护银行的总存款量
    • 确保数据一致性

成功日志

css 复制代码
msg!(
    "Deposit successful. User balance: {} -> {}, Bank total: {}",
    old_balance,
    user_account.balance,
    bank.total_balance
);

语法

  • 格式化输出交易结果

业务

  • 记录完整的交易状态变更
  • 提供审计追踪

返回成功

scss 复制代码
Ok(())

语法

  • 返回Result::Ok表示成功

业务

  • 交易完成

deposit 账户约束:

rust 复制代码
#[derive(Accounts)]
pub struct Deposit<'info> {
    #[account(mut, seeds = [b"bank"], bump)]
    pub bank: Account<'info, Bank>,
    #[account(
        init_if_needed,
        payer = user,
        space = 8 + 8,
        seeds = [b"user", user.key().as_ref()],
        bump
    )]
    pub user_account: Account<'info, UserAccount>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

结构体定义

rust 复制代码
#[derive(Accounts)]
pub struct Deposit<'info> {

语法

  • #[derive(Accounts)]标记为 Anchor 的账户验证容器。
  • <'info>:生命周期注解,保证账户数据在交易期间有效。

业务

  • 定义了 deposit 指令所需的账户集合及其约束条件。

银行主账户 bank

rust 复制代码
#[account(mut, seeds = [b"bank"], bump)]
pub bank: Account<'info, Bank>,

语法

  • mut:账户可变(需要修改 total_balance)。
  • seeds = [b"bank"]:PDA 的种子(与 initialize 中一致)。
  • bump自动验证 PDA 的 bump 值

业务

  • 访问银行的全局状态账户(Bank 结构体)。
  • 确保只有通过正确 seeds 派生的 PDA 能被修改。

用户子账户 user_account

ini 复制代码
#[account(
    init_if_needed,
    payer = user,
    space = 8 + 8,
    seeds = [b"user", user.key().as_ref()],
    bump
)]
pub user_account: Account<'info, UserAccount>,

语法

  • init_if_needed:如果账户不存在,则自动初始化。
  • payer = user:用户支付初始化费用(rent)。
  • space = 8 + 8
    • 8:Anchor 的账户头。
    • 8UserAccount.balanceu64 类型)。
  • seeds = [b"user", user.key().as_ref()]:PDA 种子(用户公钥作为派生参数)。
  • bump:自动验证 PDA bump。

业务

  • 每个用户有独立的子账户(UserAccount),存储其存款余额
  • PDA 确保用户账户地址唯一性(program_id + user_pubkey)。
  • 首次存款时自动创建账户。

用户签名账户 user

rust 复制代码
#[account(mut)]
pub user: Signer<'info>,

语法

  • mut:用户账户可变(因为要转账 SOL,需修改 lamports)。
  • Signer:必须对交易签名。

业务

  • 验证调用者是真实用户(拥有私钥)。
  • 用户需要支付:
    • 转账的 SOL 金额。
    • 子账户的初始化 rent(如果是首次存款)。

系统程序 system_program

rust 复制代码
pub system_program: Program<'info, System>,

语法

  • 显式声明依赖 Solana 系统程序。
  • 无需 mut(只读访问)。

业务

  • 用于潜在的子账户初始化(init_if_needed)。
  • Anchor 要求所有涉及账户创建的操作必须传入系统程序。

关键安全机制总结

属性/字段 语法作用 业务意义
mut 标记可变账户 允许修改余额或 lamports
seeds + bump PDA 派生与验证 确保账户地址由程序控制且唯一
init_if_needed 按需初始化子账户 首次存款时自动创建用户账户
payer = user 指定 rent 支付者 用户承担子账户初始化成本
Signer 强制签名验证 防止匿名调用

业务逻辑流程

验证账户

  • Anchor 自动检查所有账户是否符合 Deposit 结构体的约束。

存款处理

  • 如果是首次存款,初始化 user_account(用户支付 rent)。
  • user 转账 SOL 到 bank PDA。
  • 更新 user_account.balancebank.total_balance

为什么需要这些设计?

用户隔离:每个用户通过 PDA 拥有独立的子账户,避免全局状态冲突。

成本分配:用户支付自己的子账户存储费用(公平经济模型)。

安全转账 :通过系统程序 transfer 确保 SOL 转账安全可靠。

防篡改 :PDA 和 Signer 保证只有合法用户能修改自己的余额。

withdraw 指令:

rust 复制代码
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
    let user_account = &mut ctx.accounts.user_account;
    msg!(
        "Processing withdrawal of {} lamports for user: {}",
        amount,
        ctx.accounts.user.key()
    );
    require!(user_account.balance >= amount, BankError::InsufficientFunds);

    let old_balance = user_account.balance;
    let old_bank_balance = ctx.accounts.bank.total_balance;

    **ctx
        .accounts
        .bank
        .to_account_info()
        .try_borrow_mut_lamports()? -= amount;
    **ctx
        .accounts
        .user
        .to_account_info()
        .try_borrow_mut_lamports()? += amount;

    user_account.balance = user_account.balance.checked_sub(amount).unwrap();

    let bank = &mut ctx.accounts.bank;
    bank.total_balance = bank.total_balance.checked_sub(amount).unwrap();
    msg!(
        "Withdrawal successful. User balance: {} -> {}, Bank total: {} -> {}",
        old_balance,
        user_account.balance,
        old_bank_balance,
        bank.total_balance
    );

    Ok(())
}

函数签名

rust 复制代码
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {

语法

  • pub fn withdraw:公开的取款函数。
  • ctx: Context<Withdraw>:接收 Withdraw 账户上下文。
  • amount: u64:取款金额(lamports)。
  • -> Result<()>:返回执行结果。

业务

  • 用户从银行合约中提取指定数量的 SOL。

获取用户账户引用

ini 复制代码
let user_account = &mut ctx.accounts.user_account;

语法

  • &mut:获取可变引用(需要修改余额)。
  • ctx.accounts.user_account:访问 Withdraw 结构体中定义的 user_account

业务

  • 准备修改用户的链上余额数据。

日志记录取款请求

less 复制代码
msg!(
    "Processing withdrawal of {} lamports for user: {}",
    amount,
    ctx.accounts.user.key()
);

语法

  • msg!:链上日志宏。
  • 格式化输出取款金额和用户地址。

业务

  • 调试和审计用途,记录取款操作开始。

余额检查

php 复制代码
require!(user_account.balance >= amount, BankError::InsufficientFunds);

语法

  • require!:断言条件,失败时返回自定义错误。
  • BankError::InsufficientFunds:余额不足错误。

业务

  • 防止超额取款,确保合约安全性。

记录旧余额

ini 复制代码
let old_balance = user_account.balance;
let old_bank_balance = ctx.accounts.bank.total_balance;

语法

  • 拷贝当前值到局部变量。

业务

  • 为后续日志记录变更前的状态。

银行账户扣款(核心逻辑)

ini 复制代码
**ctx.accounts.bank.to_account_info().try_borrow_mut_lamports()? -= amount;

语法

  • to_account_info()将账户转为 Solana 原生账户表示
  • try_borrow_mut_lamports():借用 lamports 的可变引用。
  • ?:错误传播(如借用失败)。
  • -= amount:实际扣减 lamports。

业务

  • 从银行 PDA 中减少指定数量的 SOL。
  • 注意:直接操作 lamports 是 Solana 底层操作,需谨慎!

用户账户收款(核心逻辑)

scss 复制代码
**ctx.accounts.user.to_account_info().try_borrow_mut_lamports()? += amount;

语法:同上,但方向相反(增加用户钱包的 lamports)。

业务:将 SOL 实际转入用户钱包。

更新用户余额状态

ini 复制代码
user_account.balance = user_account.balance.checked_sub(amount).unwrap();

语法checked_sub:安全减法(防溢出)。unwrap():已知不会溢出(因前面已检查余额)。

业务:更新用户子账户的余额记录。

更新银行总余额

ini 复制代码
let bank = &mut ctx.accounts.bank;
bank.total_balance = bank.total_balance.checked_sub(amount).unwrap();

语法:获取银行账户的可变引用。同样使用安全数学运算。

业务:保持银行总余额与实际 lamports 一致。

成功日志

css 复制代码
msg!(
    "Withdrawal successful. User balance: {} -> {}, Bank total: {} -> {}",
    old_balance,
    user_account.balance,
    old_bank_balance,
    bank.total_balance
);

语法:格式化输出变更前后的状态。

业务:提供完整的审计追踪。

返回成功

scss 复制代码
Ok(())

语法 :返回 Result::Ok

业务:标记交易完成。

关键安全机制

操作 安全措施 业务意义
余额检查 require! 提前验证 防止超额取款
Lamports 操作 直接修改底层余额 避免中间状态不一致
数值计算 checked_sub 防溢出 避免算术漏洞
PDA 账户 通过 seeds 自动验证 确保只有程序能修改银行账户
签名验证 Signer 约束 防止冒充攻击

为什么这样设计?

最小化信任:所有操作通过合约逻辑强制执行,不依赖第三方。

状态一致性:amports 和余额记录同步更新,避免账目错误。

实时性:用户立即收到 SOL,无需等待。

透明性:所有操作链上可查。

withdraw 账户约束:

结构体定义

rust 复制代码
#[derive(Accounts)]
pub struct Withdraw<'info> {

语法

  • #[derive(Accounts)]:Anchor 宏,标记为账户验证容器。
  • <'info>:生命周期注解,确保账户数据在交易期间有效。

业务 :定义 withdraw 指令所需的账户及其约束条件。

银行主账户 bank

rust 复制代码
#[account(mut, seeds = [b"bank"], bump)]
pub bank: Account<'info, Bank>,

语法

  • mut:账户可变(需修改 total_balance 和 lamports)。
  • seeds = [b"bank"]:PDA 的种子(与初始化时一致)。
  • bump:自动验证 PDA 的 bump 值。

业务

  • 访问银行的全局状态账户(存储总余额)。
  • 确保只有通过正确 seeds 派生的 PDA 能被修改。

用户子账户 user_account

rust 复制代码
#[account(mut, seeds = [b"user", user.key().as_ref()], bump)]
pub user_account: Account<'info, UserAccount>,

语法

  • mut:账户可变(需修改 balance)。
  • seeds = [b"user", user.key().as_ref()]:PDA 种子(用户公钥作为派生参数)。
  • bump:自动验证 PDA bump。

业务:操作用户独立的存款子账户。PDA 确保只有程序能修改该账户(防止篡改)。

用户签名账户 user

rust 复制代码
#[account(mut)]
pub user: Signer<'info>,

语法mut:账户可变(接收 SOL 需修改 lamports)。Signer:必须对交易签名。

业务:验证调用者是真实用户(拥有私钥)。用户将接收取款的 SOL。

系统程序 system_program

rust 复制代码
pub system_program: Program<'info, System>,

语法 :显式声明依赖 Solana 系统程序。无需 mut(只读访问)。

业务:虽然取款不直接调用它,但 Anchor 推荐包含所有相关程序(潜在的安全检查)。

关键安全机制总结

属性/字段 语法作用 业务意义
mut 标记可变账户 允许修改余额或 lamports
seeds + bump PDA 派生与验证 确保账户地址由程序控制且唯一
Signer 强制签名验证 防止匿名调用
系统程序依赖 显式声明 符合 Anchor 最佳实践
相关推荐
Blockchina4 天前
第 4 章 | Solidity安全 权限控制漏洞全解析
安全·web3·区块链·智能合约·solidity
Blockchina4 天前
第十四章 | DeFi / DAO / GameFi 项目高级实战
web3·区块链·智能合约·solidity
龙兵兵科技5 天前
利用智能合约技术打破线下陪玩私单僵局的可行性分析
智能合约·软件开发·线下陪玩·同城陪玩
渗透测试老鸟-九青5 天前
记一次常规的网络安全渗透测试
运维·服务器·安全·web安全·区块链·智能合约
我是前端小学生6 天前
手摸手带你用anchor框架写一个Bank链上程序
智能合约
0x派大星7 天前
打造更安全的区块链资产管理:Solidity 多重签名机制详解
安全·web3·区块链·智能合约·solidity
Blockchina7 天前
第十五章 | Layer2、Rollup 与 ZK 技术实战解析
python·web3·区块链·智能合约·solidity
Blockchina8 天前
第 2 章 | 智能合约攻击图谱全景解析
web3·区块链·智能合约·solidity·区块链安全
Blockchina8 天前
第 1 章 | 开篇词:Dapp安全 区块链安全 Web3安全 区块链合约一旦部署,安全就是生死线
安全·web3·区块链·智能合约·solidity·合约审计