文章背景:
上篇文章,我们使用 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
结构体定义的账户(如bank
、owner
等)。
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
结构体实例),后续会设置owner
和total_balance
。
4. 设置 Bank 的 owner
ini
bank.owner = ctx.accounts.owner.key();
语法层面
bank.owner
:Bank
结构体的owner
字段(类型是Pubkey
)。ctx.accounts.owner.key()
:获取owner
账户的公钥(Signer
类型的账户)。
业务层面
- 将
Bank
的owner
设置为调用者(ctx.accounts.owner
),表示该银行合约的管理者。 - 关键点:
-
owner
在后续可用于权限控制(如仅允许owner
调用某些管理函数)。
5. 初始化 Bank 的总余额
ini
bank.total_balance = 0;
语法层面
bank.total_balance
:Bank
结构体的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 的账户标识头。32
:Bank.owner
(Pubkey
类型,固定 32 字节)。8
:Bank.total_balance
(u64
类型,固定 8 字节)。
seeds = [b"bank"]
:定义 PDA(Program Derived Address)的种子,此处为静态字符串"bank"
。bump
:自动计算 PDA 的 bump 值(避免地址冲突)。
业务层面:
- 创建或复用银行的全局状态账户,存储
owner
和total_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 的账户头。8
:UserAccount.balance
(u64
类型)。
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.balance
和bank.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 最佳实践 |