本文介绍了如何使用 Anchor 框架创建一个具有计息功能的 Token-2022 mint,通过 PDA 进行权限控制,并实现利率更新。文章详细阐述了创建、初始化、铸造以及更新利率的完整生命周期,并使用 LiteSVM 模拟时间推移,验证计息的准确性,最后提供了一个构建简易质押奖励程序的自学练习。
利息计算扩展增加了一种功能,允许 token mint 随着时间的推移累积利息。 之前,我们介绍了这个扩展,并解释了余额如何在不改变链上原始账户余额的情况下,以虚拟方式增长。 我们当时的重点是该扩展在概念上是如何运作的,以及 Solana 的客户端函数是如何计算应计利息的。
在本文中,我们将把这些知识付诸实践。 我们将使用 Anchor 构建一个管理系统,该系统在 PDA (程序派生地址) 权限下,以编程方式创建计息 token mint,从而确保只有该程序才能控制它。 该系统还将允许通过指定的利率权限更新利率。
我们将构建的程序将演示计息 token 的完整生命周期:初始化、铸造、利息累积和利率变更。 我们还将使用 LiteSVM 的时间旅行功能来随着时间的推移测试利息累积。
在本文结束时,你将对计息 token 在实践中是如何运作的有一个扎实的理解。
我们将从创建一个新的 Anchor 项目开始。 运行以下命令来初始化项目:
anchor init interest-bearing && cd interest-bearing
现在,更新你的 program/src/Cargo.toml 文件,以包含 anchor-spl 依赖项并启用 idl-build。 idl-build 功能使 Anchor 为 CPI (跨程序调用) 调用生成 IDL 定义,我们稍后将在编写测试以调用程序函数时使用这些定义。
[package]
name = "interest-bearing"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "interest_bearing"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # We added this
[dependencies]
anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } # Include this
anchor-spl = { version = "0.31.1", features = ["idl-build"] } # include this
你现在可以成功运行 anchor build,以确认你的项目已正确设置。
该项目将分为两个阶段:
1. Anchor Rust 程序
Anchor Rust 程序将处理三个核心操作:
所有这些操作都将在 Anchor 中实现为链上函数入口点。
#[program]
pub mod interest_bearing {
pub fn create_interest_bearing_mint(...) -> Result<()> { ... }
pub fn mint_tokens(...) -> Result<()> { ... }
pub fn update_rate(...) -> Result<()> { ... }
}
我们将在 programs/interest-bearing/src/lib.rs 文件中定义这些函数:
create_interest_bearing_mint: 创建一个启用了 InterestBearingConfig 扩展的 token mint,并设置利率权限。mint_tokens: 使用 PDA 作为 mint 权限,将 token 铸造到用户的账户。update_rate: 更新 mint 的年利率,仅限于利率权限。2. TypeScript 测试
TypeScript 测试将验证程序是否可以:
现在我们了解了项目结构,让我们来实现链上程序本身。
我们先导入所需的 Anchor 和 Token-2022 依赖项,并在 program/interest-bearing/src/lib.rs 文件中声明程序 ID:
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, CreateAccount};
use anchor_spl::{
token_2022::{
initialize_mint2,
spl_token_2022::{
extension::{ExtensionType},
pod::PodMint,
},
InitializeMint2, Token2022,
},
token_interface::{Mint, TokenAccount, mint_to, MintTo},
token_2022_extensions::interest_bearing_mint::{
interest_bearing_mint_initialize,
interest_bearing_mint_update_rate,
InterestBearingMintInitialize,
InterestBearingMintUpdateRate,
},
};
declare_id!("5fRtYEKp81rfRRqcYxp9XMnucgvtt6JxQ5GDDQy2TMtx");
请注意,我们没有直接安装 spl-token-2022——我们使用的是 Anchor 的重新导出。 混合使用两者可能会导致版本不匹配和运行时冲突。
最后,运行 anchor keys sync 以确保 declare_id! 宏中的程序 ID 与 Anchor.toml 中定义的密钥对匹配。
我们已经准备好所有依赖项,现在让我们设置工作流程来创建和初始化计息 token mint。
我们现在创建 create_interest_bearing_mint 函数:
pub fn create_interest_bearing_mint(...) -> Result<()> { ... }
该函数执行四个步骤来设置一个新的启用 InterestBearingConfig 扩展的 Token-2022 mint。 这些步骤是:
InterestBearingConfig 账户大小InterestBearingConfig 扩展initialize_mint2 函数第 1 步:计算所需的账户大小
在 Solana 中创建账户时,你需要指定账户的大小并相应地支付租金。
我们将使用我们之前导入的 ExtensionType 中的 try_calculate_account_len 函数来自动计算保存基本 mint 数据和扩展数据所需的账户大小。 这确保账户被分配足够的空间用于 InterestBearingConfig 扩展。
let mint_size = ExtensionType::try_calculate_account_len::<Mint>(&[\
ExtensionType::InterestBearingConfig,\
])?;
在第一部分中,我们讨论了如何手动计算扩展数据,但我们将在此处使用 try_calculate_account_len。 使用 try_calculate_account_len 是标准做法,它允许我们一次性计算 Mint 账户和扩展数据的准确大小。
第 2 步:创建和资助 mint 账户
现在我们有了计算准确大小的机制,我们将使用系统程序手动创建 mint 账户,并为其提供免租金的 lamports ( 当一个账户持有相对于其大小而言足够多的 lamports 时,它将变为“免租金”,并且永远不会被收取租金或删除)。
Anchor 不会自动执行此步骤,因为 Token-2022 mint 需要自定义大小以适应扩展。 账户上的 #[account(init)] 属性假定一个固定大小(对于标准 SPL Token mint 有效),但 Token-2022 mint 则根据它们包含的扩展而有所不同。 为了正确处理这个问题,你必须自己计算所需的空间并手动创建账户。
下面的代码使用精确的空间和 lamports 创建 mint 账户,使其免于租金。
Rent::get()?.minimum_balance(mint_size) 基于账户的大小计算使账户免于租金所需的最小 lamports。system_program::create_account 然后分配并资助该账户,并将所有权分配给 Token-2022 程序 ( token_program.key())。这确保了在任何 Token-2022 指令初始化它之前,mint 账户被正确调整大小、免于租金并归正确的程序所有。
// 2) Create the mint account with correct space and rent
let lamports = Rent::get()?.minimum_balance(mint_size);
system_program::create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.mint.to_account_info(),
},
),
lamports,
mint_size as u64,
&ctx.accounts.token_program.key(),
)?;
我们将在本文的后面定义完整的 CreateInterestBearingMint 账户结构,其中 &ctx 指的是上面的代码。
第 3 步:初始化 InterestBearingConfig 扩展
接下来,我们通过设置利率权限和初始利率(以基点为单位)来初始化 InterestBearingConfig ****扩展。
此步骤必须在 ****初始化基本 mint 之前进行,因为必须首先设置扩展——否则,mint 的布局将与预期的账户大小不匹配,并且 initialize_mint2 将失败。
// 3) Initialize the interest-bearing extension BEFORE base mint init
interest_bearing_mint_initialize(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
InterestBearingMintInitialize {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
},
),
Some(ctx.accounts.rate_authority.key()),
rate_bps,
)?;
我们在此处使用了 Some(ctx.accounts.rate_authority.key()),因为利率权限是可选的。 如第一部分所述,如果没有提供利率权限,该字段将填充零,使利率不可变。
第 4 步:运行标准 initialize_mint2 函数
最后,下面的代码使用标准 initialize_mint2 CPI 初始化基本 mint 本身。 这设置了 mint 的小数位数,将 PDA 分配为 mint 和冻结权限,并最终确定了 Token-2022 mint 的配置。
由于程序无法保存私钥,因此 PDA 充当 mint 的权限。 每当程序需要代表此 PDA 签名时(例如,在铸造新 token 时),它必须使用相同的种子和跳转组合 ( [b"mint-authority", &[bump]]) 重新派生 PDA。
Anchor 通过 ctx.bumps 公开此跳转。
跳转是在 PDA 派生期间添加的单字节值 (0–255)。 它确保生成的地址无法从任何私钥生成。 它还必须包含在 PDA 签名验证期间的签名者种子中; 否则,验证将失败。
我们还将 mint 权限和 冻结权限都设置为 PDA,以确保只有程序的逻辑才能铸造或冻结 token。
// 4) Initialize base mint (decimals, authorities)
let mint_auth_bump = ctx.bumps.mint_authority;
let signer_seeds: &[&[&[u8]]] = &[&[b"mint-authority", &[mint_auth_bump]]];
initialize_mint2(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
InitializeMint2 {
mint: ctx.accounts.mint.to_account_info(),
},
signer_seeds,
),
decimals,
&ctx.accounts.mint_authority.key(),
Some(&ctx.accounts.mint_authority.key()),
)?;
CreateInterestBearingMint 账户上下文下面是定义我们到目前为止使用的 CreateInterestBearingMint 函数的账户上下文的结构。
请注意,mint 被声明为 UncheckedAccount 而不是 InterfaceAccount<Mint>(Anchor 对 AccountInfo 的包装器,它会自动验证账户以确保它是一个已初始化的 token mint)。
我们在此处使用 UncheckedAccount,因为我们需要使用扩展空间创建 mint,并且 Anchor 在初始化完成后才能将其验证为 Mint。
该结构使用其种子和跳转定义了 mint_authority PDA。 完成后,程序逻辑可以铸造或冻结 token,但没有外部密钥对可以。
该结构还定义了我们使用的其他账户; 我们添加了注释来指定它们。
#[derive(Accounts)]
pub struct CreateInterestBearingMint<'info> {
/// CHECK: This account is created manually as a Token-2022 mint with extensions.
// CHECK:此账户是使用扩展手动创建为 Token-2022 mint 的。
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: PDA account used as mint and freeze authority
// CHECK:PDA 账户用作 mint 和冻结权限
#[account(\
seeds = [b"mint-authority"],\
bump\
)]
pub mint_authority: UncheckedAccount<'info>,
/// Raw mint account to be created with extension space
// 要使用扩展空间创建的原始 mint 账户
/// CHECK: We trust the token program to validate this is a proper mint account.
// CHECK:我们相信 token 程序会验证这是一个正确的 mint 账户。
#[account(mut, signer)]
pub mint: UncheckedAccount<'info>,
/// Token-2022 program
// Token-2022 程序
pub token_program: Program<'info, Token2022>,
pub system_program: Program<'info, System>,
/// Signer that will control interest rate updates
// 将控制利率更新的签名者
pub rate_authority: Signer<'info>,
}
我们到目前为止讨论的创建 token mint 的完整代码如下所示:
use anchor_lang::prelude::*;
use anchor_lang::system_program::{self, CreateAccount};
use anchor_spl::{
token_2022::{
initialize_mint2,
spl_token_2022::{
extension::{ExtensionType},
pod::PodMint,
},
InitializeMint2, Token2022,
},
token_interface::{Mint, TokenAccount, mint_to, MintTo},
token_2022_extensions::interest_bearing_mint::{
interest_bearing_mint_initialize,
interest_bearing_mint_update_rate,
InterestBearingMintInitialize,
InterestBearingMintUpdateRate,
},
};
declare_id!("5fRtYEKp81rfRRqcYxp9XMnucgvtt6JxQ5GDDQy2TMtx");
#[program]
pub mod interest_bearing {
use super::*;
pub fn create_interest_bearing_mint(
ctx: Context<CreateInterestBearingMint>,
rate_bps: i16,
decimals: u8,
) -> Result<()> {
msg!("Create interest-bearing mint @ {} bps", rate_bps);
// msg!("创建计息 mint @ {} bps", rate_bps);
// 1) Compute mint size including extension header + InterestBearingConfig
// 1) 计算 mint 大小,包括扩展标头 + InterestBearingConfig
let mint_size = ExtensionType::try_calculate_account_len::<PodMint>(&[\
ExtensionType::InterestBearingConfig,\
])?;
// 2) Create the mint account with correct space and rent
// 2) 使用正确的空间和租金创建 mint 账户
let lamports = Rent::get()?.minimum_balance(mint_size);
system_program::create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.mint.to_account_info(),
},
),
lamports,
mint_size as u64,
&ctx.accounts.token_program.key(),
)?;
// 3) Initialize the interest-bearing extension BEFORE base mint init
// 3) 在基本 mint 初始化之前初始化计息扩展
interest_bearing_mint_initialize(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
InterestBearingMintInitialize {
token_program_id: ctx.accounts.token_program.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
},
),
Some(ctx.accounts.rate_authority.key()),
rate_bps,
)?;
// 4) Initialize base mint (decimals, authorities)
// 4) 初始化基本 mint(小数位数、权限)
let mint_auth_bump = ctx.bumps.mint_authority;
let signer_seeds: &[&[&[u8]]] = &[&[b"mint-authority", &[mint_auth_bump]]];
initialize_mint2(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
InitializeMint2 {
mint: ctx.accounts.mint.to_account_info(),
},
signer_seeds,
),
decimals,
&ctx.accounts.mint_authority.key(),
Some(&ctx.accounts.mint_authority.key()),
)?;
Ok(())
}
#[derive(Accounts)]
pub struct CreateInterestBearingMint<'info> {
/// CHECK: This account is created manually as a Token-2022 mint with extensions.
// CHECK:此账户是使用扩展手动创建为 Token-2022 mint 的。
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: PDA account used as mint and freeze authority
// CHECK:PDA 账户用作 mint 和冻结权限
#[account(\
seeds = [b"mint-authority"],\
bump\
)]
pub mint_authority: UncheckedAccount<'info>,
/// Raw mint account to be created with extension space
// 要使用扩展空间创建的原始 mint 账户
/// CHECK: We trust the token program to validate this is a proper mint account.
// CHECK:我们相信 token 程序会验证这是一个正确的 mint 账户。
// #[account(mut)]
#[account(mut, signer)]
pub mint: UncheckedAccount<'info>,
/// Token-2022 program
// Token-2022 程序
pub token_program: Program<'info, Token2022>,
pub system_program: Program<'info, System>,
/// Signer that will control interest rate updates
// 将控制利率更新的签名者
pub rate_authority: Signer<'info>,
}
}
现在我们已经创建了 token mint 并初始化了扩展,让我们继续实现 mint_tokens 函数。
mint_tokens 函数使用 PDA 作为 mint 权限,将 token 铸造到用户的账户。
以下是 mint_tokens 函数的功能:
mint_to CPI。 它通过 CpiContext::new_with_signer 传递签名者种子,运行时将 PDA 识别为授权签名者,并将指定数量的 token 铸造到接收者的 token 账户。
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
// Fetch the bump for the PDA so we can recreate the same signer seeds
// 获取 PDA... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!