本篇文章详细介绍了Anchor框架的init_if_needed
宏,提供了一种在一次事务中初始化账户并写入数据的方法。文中阐述了该宏的便利性与可能引发的重初始化攻击风险,特别是在账户状态和lamport余额的处理上。同时,通过示例代码和测试用例,深入分析了如何安全地使用这些功能,以避免潜在的错误和安全隐患。
在之前的教程中,我们必须在单独的交易中初始化一个账户,然后才能对其写入数据。我们可能希望能够在一个交易中初始化一个账户并对其写入数据,以简化用户的操作。
Anchor 提供了一个方便的宏 init_if_needed
,顾名思义,如果账户不存在,则会初始化该账户。
下面的示例计数器不需要单独的初始化交易,它会立即开始将“1”添加到 counter
存储中。
Rust:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("9DbiqCqtqgP3NYufxBakbeRd7JyNpNYbsm6Jqrn8Z2Hn");
#[program]
pub mod init_if_needed {
use super::*;
pub fn increment(ctx: Context<Initialize>) -> Result<()> {
let current_counter = ctx.accounts.my_pda.counter;
ctx.accounts.my_pda.counter = current_counter + 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init_if_needed,
payer = signer,
space = size_of::<MyPDA>() + 8,
seeds = [],
bump
)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyPDA {
pub counter: u64,
}
Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { InitIfNeeded } from "../target/types/init_if_needed";
describe("init_if_needed", () => {
// 配置客户端以使用本地集群。
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.InitIfNeeded as Program<InitIfNeeded>;
it("已初始化!", async () => {
const [myPda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
await program.methods.increment().accounts({myPda: myPda}).rpc();
await program.methods.increment().accounts({myPda: myPda}).rpc();
await program.methods.increment().accounts({myPda: myPda}).rpc();
let result = await program.account.myPda.fetch(myPda);
console.log(`counter是 ${result.counter}`);
});
});
当我们尝试使用 anchor build
构建这个程序时,将会遇到以下错误:
为了消除错误 init_if_needed requires that anchor-lang be imported with the init-if-needed cargo feature enabled
,我们可以打开 programs/<anchor_project_name>
中的 Cargo.toml
文件并添加以下行:
[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] }
但在我们只是移除错误之前,我们应该了解什么是重新初始化攻击及其如何发生。
如果我们尝试初始化一个已经初始化的账户,交易将会失败。
从 Anchor 的角度来看,如果该账户的 lamport 余额为零 或者该账户由系统程序拥有,那么它就是未初始化的。
由系统程序拥有或具有零 lamport 余额的账户可以再次初始化。
为了说明这一点,我们有一个具有典型 initialize
函数的 Solana 程序(它使用的是 init
,而不是 init_if_needed
)。它还具有 drain_lamports
函数和 give_to_system_program
函数,这两个函数的作用如其名所示:
use anchor_lang::prelude::*;
use std::mem::size_of;
use anchor_lang::system_program;
declare_id!("FC467mPCCKXG97ut1WdLLi55vuAcyCW8AD1vid27bZfn");
#[program]
pub mod reinit_attack {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn drain_lamports(ctx: Context<DrainLamports>) -> Result<()> {
let lamports = ctx.accounts.my_pda.to_account_info().lamports();
ctx.accounts.my_pda.sub_lamports(lamports)?;
ctx.accounts.signer.add_lamports(lamports)?;
Ok(())
}
pub fn give_to_system_program(ctx: Context<GiveToSystemProgram>) -> Result<()> {
let account_info = &mut ctx.accounts.my_pda.to_account_info();
// assign 方法更改拥有者
account_info.assign(&system_program::ID);
account_info.realloc(0, false)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct DrainLamports<'info> {
#[account(mut)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = 8, seeds = [], bump)]
pub my_pda: Account<'info, MyPDA>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct GiveToSystemProgram<'info> {
#[account(mut)]
pub my_pda: Account<'info, MyPDA>,...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!