Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

计息代币第二部分

  • RareSkills
  • 发布于 2025-10-18 18:00
  • 阅读 1207

本文介绍了如何使用 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-buildidl-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 程序
  2. 以及 TypeScript 测试

1. Anchor Rust 程序

Anchor Rust 程序将处理三个核心操作:

  • 创建和初始化一个新的计息 mint,并将 PDA 设置为 mint 权限
  • 铸造计息 token
  • 通过利率权限更新利率。

所有这些操作都将在 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 文件中定义这些函数:

  1. create_interest_bearing_mint: 创建一个启用了 InterestBearingConfig 扩展的 token mint,并设置利率权限。
  2. mint_tokens: 使用 PDA 作为 mint 权限,将 token 铸造到用户的账户。
  3. update_rate: 更新 mint 的年利率,仅限于利率权限。

2. TypeScript 测试

TypeScript 测试将验证程序是否可以:

  • 创建一个计息 mint
  • 在 PDA 权限下铸造 token
  • 通过利率权限更新利率
  • 准确显示虚拟利息累积。

实现 Anchor Rust 程序

现在我们了解了项目结构,让我们来实现链上程序本身。

我们先导入所需的 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。

i. 创建和初始化计息 token mint

我们现在创建 create_interest_bearing_mint 函数:

pub fn create_interest_bearing_mint(...) -> Result<()> { ... }

该函数执行四个步骤来设置一个新的启用 InterestBearingConfig 扩展的 Token-2022 mint。 这些步骤是:

  • 第 1 步:计算 InterestBearingConfig 账户大小
  • 第 2 步:创建 mint 账户并为其提供用于租金的 lamports
  • 第 3 步:初始化 InterestBearingConfig 扩展
  • 第 4 步:运行标准 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())。
  • CPI 上下文指定 lamports 来自付款人,并且正在创建的新账户是 mint。

这确保了在任何 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 函数。

ii. 创建 Mint token 函数

mint_tokens 函数使用 PDA 作为 mint 权限,将 token 铸造到用户的账户。

以下是 mint_tokens 函数的功能:

  • 它首先检索 PDA 的跳转并重建验证所需的签名者种子。
  • 然后,它调用 Token-2022 程序的 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...

剩余50%的内容订阅专栏后可查看

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

0 条评论

请先 登录 后评论