Solana Token-2022 标准规范

本文深入探讨了 Solana 的 Token-2022 标准,它是 SPL Token 程序的一个向后兼容的新版本,支持通过扩展实现额外的功能。

Token-2022 是 SPL Token 程序的一个新的向后兼容版本,它通过 扩展 的形式支持额外的特性。这些扩展的字节码是 Token-2022 程序本身的一部分,没有部署单独的程序。为特定 token 激活的字节码由 mint 或 token 账户中启用的扩展决定。

例如,在最初的 SPL Token 程序中,添加诸如 token 名称、符号或 logo URL 等元数据需要像 Metaplex 这样的外部程序。但在 Token-2022 中,你可以通过在 token mint 上启用元数据扩展来添加元数据。

在本文中,我们将学习 Token-2022 在底层是如何工作的,以及为了支持扩展而做了哪些更改。我们将演练一个使用 Token-2022 创建带有扩展的 Solana token 的实际例子。

Token-2022 的架构

首先,我们将研究 Token-2022 在底层是如何扩展原始 SPL Token 程序的,账户布局如何保持兼容,以及指令集如何在不破坏现有程序的情况下增长。

超集设计和向后兼容性

Token-2022 是 SPL Token 程序的一个直接替代品。你之前可以执行的每个操作——铸造、转移、销毁、冻结、更改权限——仍然完全一样地工作。这是因为 Token-2022 保留了原始 SPL mint 账户的前 82 个字节和 token 账户的前 165 个字节,其中包含诸如供应量、小数位数、所有者、冻结权限和金额等字段。 下面是原始 SPL mint 账户的布局:

一个展示数据如何在 SPL 账户中序列化的图表

一个展示 SPL mint 账户前 82 个字节的内存布局的图表

原始程序 (SPL) 仅反序列化和处理该数据;Token-2022 反序列化相同的区域,然后继续读取其后的扩展。

扩展是内置于 Token-2022 程序中的可选特性,你可以在 mint 账户或 token 账户上启用它们。下图显示了 Token-2022 mint TLV 布局:原始 SPL mint 的固定 82 字节,后跟存储扩展数据的可变大小 TLV 区域。

一个展示 Token-2022 中的数据布局如何与 SPL 不冲突的图表。

Token-2022 账户布局和 TLV 扩展

原始 SPL Token 程序具有固定的二进制布局,其中每个字段的大小、类型和值都是预定的。Token-2022 在保留的前 82 到 165 字节区域中使用相同的固定二进制布局,但除此之外,它使用可变的类型-长度-值 (TLV) 编码方案来存储扩展。

类型–长度–值 (TLV) 是一种数据序列化方案。数据中的每个对象包含三个部分:

  • 类型:一个标识符,指定数据代表什么。
  • 长度:数据的字节大小。
  • :实际的数据字节。

因为每个条目都说明了它的类型和长度,所以程序可以读取类型来理解它正在解析的内容,读取精确到该数量的字节作为值,然后使用长度直接跳到下一个条目。

即使类型未知,程序仍然可以使用长度来跳过它。

例如,在下图中,类型为 12 的条目的长度为 2 字节,因此它的值字段包含两个字节。如果程序无法识别类型 12,它可以跳过这 2 个字节并直接移动到下一个条目,类型 20

一个说明类型长度值的图表

一个说明类型长度值的图表

Token-2022 使用这种 TLV 编码方案实现。每个扩展都表示为一个 TLV 条目,并且具有以下上下文:

  • 类型 是扩展的唯一 ID。
  • 长度 是序列化的扩展数据的大小(以字节为单位)。
  • 是序列化的扩展数据本身。

这种结构允许多个扩展按顺序打包到同一个账户中,而不会发生冲突。

一个展示 TLV 如何端到端布局的图表

为了说明 TLV 编码在实践中是如何工作的,让我们来看两个扩展:ImmutableOwnerMetadataPointer

ImmutableOwner TLV 布局

ImmutableOwner 扩展是一个 Token-2022 扩展,它可以防止 token 账户的所有者在创建后被更改。 我们现在来看看 TLV 布局,并在本文后面讨论更多关于它的用法。

ImmutableOwner 具有以下 TLV 属性:

  • 类型 (T): 0x0a (ImmutableOwner 的唯一 ID)
  • 长度 (L): 0x01 0x00 0x00 0x00 (数字 1 编码为 4 字节小端整数)
  • 值 (V): 0x01 (该扩展存储一个字节的数据值,可以是 01)

Rust 定义是一个空结构体:

pub struct ImmutableOwner {}

虽然该结构体没有字段,但 TLV 编码仍然为 V 保留一个字节,以表示该标志是否已启用 (1 为启用,0 为禁用)。 这就是为什么该条目具有非零长度的原因。

因此,它的 TLV 布局如下所示:

ImmutableOwner 字节布局

MetadataPointer TLV 布局

MetadataPointer 扩展定义了 token 的链下元数据的位置以及谁可以更新它。

ImmutableOwner 扩展不同,MetadataPointer 包含值:两个公钥值(authoritymetadata_address)。

以下是 Rust 结构体的样子:

struct MetadataPointer {
    authority: PubKey;
    metadata_address: PubKey;
}

在 TLV 中,布局将包含以下信息:

  • 0x1a: MetadataPointer 的类型 ID (T)
  • 0x40 0x00 0x00 0x00: 长度 (L) = 64 字节 (小端序)
  • <64 bytes>: 值 (V),序列化的 authoritymetadata_address 指向元数据的公共地址。

因为 V 包含一个 authoritymetadata_address 公钥,所以让我们使用示例 32 字节公钥来表示它们,以便我们更好地了解布局的样子。

假设账户的 authority 公钥是:

7c4YH58z6Yd1H5pa9vHqPqN8P3f9DuzGcbj2duq5Vn6a

metadata_address 公钥是:

9A4q8Xzj8cQ6w6sKuS27rrR2i1cC6VnV4c7pg1Zg1Vgk

在 Solana 上,公钥是 Base58 编码的 32 字节值。值 (V) 是 authoritymetadata_address 的串联。 当两个公钥都从 Base58 解码为其原始字节,然后以十六进制形式写入时,生成的 64 字节序列将为:

// authority (32 bytes)
0x62 0x21 0x73 0xa4 0x94 0x0c 0x4c 0x3c 0x29 0x7a 0x7f 0x3c 0x4f 0xc1 0x12 0x3f
0x3b 0x34 0xc6 0x51 0x3f 0x3e 0x24 0x23 0xf3 0x1c 0xaa 0x88 0x83 0x44 0xa3 0x37
// metadata_address (32 bytes)
0x79 0x30 0x0d 0x97 0x56 0x47 0xc2 0x18 0x79 0x35 0x0d 0xe6 0x18 0x9f 0x80 0xec
0xd6 0xca 0x36 0xa5 0xb1 0x77 0x5a 0xa8 0xe4 0x45 0x66 0x7b 0x85 0xf3 0x32 0xe1

假设 Token-2022 账户同时初始化了 ImmutableOwnerMetadataPointer 扩展,以下是 TLV 布局的样子:

Token-2022 TLV 布局,展示了端到端布局的 ImmutableOwner 和 MetadataPointer

读取账户时,程序可以迭代 TLV 部分,仅选择性地解码它识别的扩展。 未知扩展类型通过使用它们声明的长度跳过。

让我们考虑一下程序将如何处理未知扩展 UnknownExtension

  • 程序读取 UnknownExtension 的 TLV 条目:

    • T = 0x0b (一个未知类型)
    • L = 0x14 0x00 0x00 0x00 (20,小端序)
    • V = 0x4…0xed (20 字节长,如 L 所指定)
  • 由于该类型未被识别,程序不会尝试解释该值
  • 但它仍然读取长度并按该字节数向前跳过(在本例中为 20)
  • 然后,它继续执行下一个 TLV 条目(如果有)

现在假设未知扩展具有 64 字节的值,程序将从 L 读取值 64,然后向前跳过 64 字节(在 V 上方)以找到下一个 T。 这种方法使 Token-2022 具有向前兼容性;未来的扩展不会破坏现有程序。

Token-2022 指令兼容性和新功能

Solana 指令是对链上程序的调用,它由三个字段组成:

  • 程序 ID,要调用的链上程序的公钥,
  • 程序将读取或写入的帐户列表
  • 指令数据 - 由程序定义的任意字节序列。

原始 SPL Token 程序有 25 个唯一指令。 Token-2022 支持所有这些指令,并在第 25 个指令之后添加新指令以启用新的扩展功能。

换句话说,Token-2022 中的现有 token 指令(如 MintToTransferBurn)的行为与 SPL 中的完全相同。

这是一个示例指令布局,它告诉 token 程序将 100 个 token 从 mint 账户铸造到目标 token 账户:

Token-2022 中一个 mint 账户中的数据

应用程序可以通过简单地更改其交易中的程序 ID 来采用 Token-2022。

以下是超出 Token 程序的原始 25 个指令的 Token-2022 指令列表。 命名为匹配它们初始化或管理的扩展名的 token 指令:

25: InitializeMintCloseAuthority
26: TransferFeeExtension
27: ConfidentialTransferExtension
28: DefaultAccountStateExtension
29: Reallocate
30: MemoTransferExtension
31: CreateNativeMint
32: InitializeNonTransferableMint
33: InterestBearingMintExtension
34: CpiGuardExtension
35: InitializePermanentDelegate
36: TransferHookExtension
37: ConfidentialTransferFeeExtension
38: WithdrawExcessLamports
39: MetadataPointerExtension
40: GroupPointerExtension
41: GroupMemberPointerExtension
42: ConfidentialMintBurnExtension
43: ScaledUiAmountExtension
44: PausableExtension

实现模式以及如何创建一个 Token-2022 token

在 Token-2022 中,所有扩展都必须在初始化 mint 或 token 账户之前指定,以便可以为它们的数据分配足够的空间。 一旦初始化,就无法添加其他扩展。

你可以使用 spl-token CLI 创建带有扩展的 Token-2022 token。 它将计算所需的帐户大小,将每个扩展的 TLV 条目写入 mint 账户,并最终在幕后使用 InitializeMint2 指令初始化 mint。

这是 CLI 模板的样子:

spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  create-token <扩展标识>

假设我们想向一个 mint 添加两个扩展:一个定义 token 的固定利率 (InterestBearingConfig),另一个使 mint 能够引用链下元数据 (MetadataPointer)。 我们可以运行以下命令,附加 —-interest-rate—-enable-metadata 标志。 下面命令中的 5 是利率:

spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  create-token --interest-rate 5 --enable-metadata

此命令创建一个同时启用了 InterestBearingConfigMetadataPointer 扩展的 mint 账户。 在内部,它使用 ExtensionType::try_calculate_account_len::<Mint>(&[InterestBearingConfig, MetadataPointer])?; 分配完整的账户大小,立即写入利率参数,隐式地初始化 InterestBearingConfig 扩展,并为元数据保留一个 TLV 插槽。 现在,我们必须手动初始化元数据扩展。

try_calculate_account_len (来自 token-2022 库) 函数根据所选扩展计算所需的总空间,并且每个扩展的初始化指令在最终 mint 初始化锁定账户结构之前配置其特定参数。

如果你运行我们之前提到的 spl-token 命令来启用扩展,你将看到如下输出:

一个截图,展示了 spl-token cli 在终端中运行,以创建一个使用利率扩展和元数据扩展的 Token-2022。

在此阶段:

  • 利率扩展已完全初始化,利率为 5
  • 元数据扩展具有保留的 TLV 插槽,但尚未写入任何元数据内容。

你会注意到以下消息:“要初始化 mint 中的元数据,请运行 spl-token initialize-metadata 5bL18vT46c7SkdN37F3pb1GdxsN8kTcZCPoRcYj6cS5w <YOUR_TOKEN_NAME> <YOUR_TOKEN_SYMBOL> <YOUR_TOKEN_URI>,并使用 mint 授权签署。" 在响应中。

我们将运行该命令来最终确定元数据初始化并填充已分配的元数据 TLV 块。

spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
  initialize-metadata 5bL18vT46c7SkdN37F3pb1GdxsN8kTcZCPoRcYj6cS5w \
    "MyToken" "MTKN" "https://example.com/mytoken.json"

结果将如下所示:

一个 spl-token cli 命令,用于初始化 token 的元数据。

如果你之前省略了 --enable-metadata,则此步骤将失败,因为一旦初始化 mint,就无法添加新的 TLV 空间。 因此,你将看到如下错误:

终端的屏幕截图,说明了如果在之前的命令中没有运行 `--enable-metadata` 就初始化元数据扩展会发生什么

将扩展添加到 mint 所需步骤的摘要包括:

  • 通过为扩展分配空间来启用扩展。
  • 初始化扩展。 你可以一次初始化多个扩展。

对于像利率扩展这样的扩展,它允许你一次性提供所需的参数(我们上面使用 --interest-rate 5 完成了此操作),你将一次启用和初始化扩展。

以下是一些 token 扩展以及你可以使用哪些标志在 CLI 上启用它们。

扩展 CLI 标志
Mint 关闭授权 –enable-close
转移费用 –transfer-fee-basis-points –transfer-fee-maximum-fee
不可转移 –enable-non-transferable
计算利息 –interest-rate
永久委托 –enable-permanent-delegate
转移Hook –transfer-hook
元数据 –enable-metadata
元数据指针 –metadata-address
保密传输 –enable-confidential-transfers auto

Solana Token-2022 源代码中的 ExtensionType 枚举定义了所有可用的扩展。

组合扩展

你已经了解了我们在创建 mint 账户时如何组合 Interest BearingMetadata 扩展。 你还看到了如何启用和初始化扩展。

但是,并非所有扩展都可以与其他扩展组合。 例如,你不能组合 NonTransferableTransferHook,因为如果 token 无法转移,则转移Hook没有有意义的工作。 SVM 和开发工具强制执行此规则,因此尝试组合这两个扩展将导致执行失败。

此外,你不能组合 ConfidentialTransferTransferHook 扩展,因为保密转移加密金额并限制可见性。 由于转移Hook依赖于读取转移的金额,因此两者无法协同工作。

以下是一些其他无法协同工作的扩展组合:

  • ConfidentialTransferTransferFeeConfig
  • ConfidentialTransferPermanentDelegate

总结

在我们进入本文的下一部分之前,让我们总结一下我们到目前为止讨论的内容:

  • Token 账户的前 165 个字节和 Mint 账户的前 82 个字节都保留在 Token-2022 中。
  • 扩展使用 TLV(类型-长度-值)编码格式存储。
  • Token-2022 向原始 SPL Token 程序的 25 个指令添加了 20 个新指令。
  • 开发人员必须在账户创建期间预先为所有要启用的扩展分配足够的空间。
  • 扩展必须在 mint 创建之前初始化

ImmutableOwner 和 Non-Transferable 扩展

我们现在将讨论两个扩展:(ImmutableOwnerNonTransferable),并演示如何组合它们来构建凭据颁发程序。

Immutable Owner 扩展

旧版 Token 程序允许你使用 SetAuthority 指令将 Token 账户的所有者更改为另一个账户。 Token-2022 使你能够使用 ImmutableOwner 扩展使所有者不可更改。 ImmutableOwner 扩展永久锁定 Token 账户的所有权。 这意味着,一旦创建了具有此扩展的 token 账户,它将永久属于指定的钱包。

以下是一个场景,其中 ImmutableOwner 扩展可以防止网络钓鱼攻击,这些攻击可能在使用旧版 Token 程序时发生,该程序允许在账户创建后更改所有权。

旧版 Token 程序 (没有 ImmutableOwner):

  1. Alice 创建一个关联 token 账户 (ATA),称为 AliceTokenAccount,它来自她的钱包地址和一个特定的 mint。
  2. 她被骗签署了一笔交易,将该 ATA 的所有权转移给 Bob。
  3. 后来,一个应用程序使用标准派生函数计算 Alice 的 ATA 地址。该函数仍然返回 AliceTokenAccount,因为它不检查所有权更改。
  4. 该应用程序将 token 发送到该地址,假设 Alice 拥有它。但 Bob 现在控制着它,所以 Bob 收到了这些 token。
  5. 未来所有转移到 Alice 的 “ATA” 的交易也将发送给 Bob。

使用 Token-2022 的 ImmutableOwner 扩展(默认在 ATA 上启用):

  • 第 2 步中的交易将 失败 – 所有权无法更改
  • 即使 Alice 被骗签署了恶意交易,她仍然可以控制她的 ATA
  • 发送到她的派生的 ATA 地址的 token 将始终到达她

以下是 Token-2022 如何使用 ImmutableOwner 扩展强制执行此约束

当执行 SetAuthority 指令(用于在旧版 SPL token 程序中更改 token 账户所有权的指令)时,它会调用 process_set_authority 函数,该函数检查存在的授权类型。 如果授权类型为 AuthorityType::AccountOwner,它会触发 ImmutableOwner 检查,返回一个错误并阻止所有权更改:

Token-2022 Rust 代码库的屏幕截图,显示 token 账户所有者无法更改。

此实现可以在 GitHub 上的 Token-2022 程序的核心逻辑 中找到。

ImmutableOwner 扩展始终默认为使用 Token-2022 程序创建的 ATA 启用。

当你使用 Token-2022 的 标准 ATA 程序创建 ATA 时,ATA 程序会自动在账户初始化过程中包含 ImmutableOwner 扩展。

无论你是否明确指定它,都会发生这种情况——它内置于 ATA 程序用于 Token-2022 账户的逻辑中。 这与使用系统 createAccount 指令手动创建的 token 账户不同,在后一种情况下,如果需要,你需要显式初始化 ImmutableOwner 扩展。

这种设计确保了最常见的 token 账户模式 (ATA) 默认获得不可变所有权的安全优势。

不可转移扩展

NonTransferable 扩展完全禁用 token 转移。 你仍然可以将 token 铸造到账户中,但一旦收到,它们就无法发送到任何其他地方。

对此类账户的唯一有效操作是销毁 token (对于来自 Ethereum 背景的人来说,Solana 将 token 销毁视为与转移不同的操作) 或关闭空账户。

这对于构建 token 代表不可转移资产的系统非常有用。 例如,证书或链上身份标记不应在用户之间转移,或者用户欠协议的债务也不应转移。 在这种情况下,你将使用 NonTransferable 扩展发行 NFT。

Token-2022 在程序逻辑中强制执行此限制。 如果你尝试从持有不可转移 token 的账户进行转移,该指令将失败并显示错误。

你可以在 processor.rs 中看到此检查,该程序检查账户中是否存在 NonTransferableAccount 扩展,如果存在,则拒绝转移并显示 NonTransferable 错误。

Token-2022 代码库的屏幕截图,显示如果启用了 NonTransferable 扩展,则无法转移 token。

不可转移的 token 只能铸造到具有不可变所有者的账户中。 这是为了防止通过账户所有权变更进行间接 token 转移。

当使用 NonTransferable 扩展创建 mint 时,为该 mint 创建的任何 token 账户将自动继承两个扩展:NonTransferableAccount (阻止转移) 和 ImmutableOwner (阻止所有权更改)。 以下是如何在 Token-2022 程序逻辑中强制执行此操作,并且可以在我们之前在 GitHub 上提到的同一 文件 中找到。

if mint.get_extension::<NonTransferable>().is_ok()
    && destination_account.get_extension::<ImmutableOwner>().is_err()
{
    return Err(TokenError::NonTransferableNeedsImmutableOwnership.into());
}

示例:使用 ImmutableOwnerNonTransferable 扩展构建一个最小凭据颁发程序

本节将介绍:

  • 如何定义一个同时具有 ImmutableOwnerNonTransferable 扩展的 mint。
  • 如何向接收者颁发不可转移的凭据 token
  • 以及 Token-2022 程序逻辑如何确保凭据无法转移

免责声明:这里描述的 Anchor 程序仅用于教育目的。 对于生产环境,请确保你的程序包含完整的验证、安全检查并接受审查。

项目设置

通过运行命令 anchor init credentials 创建一个新的 Anchor 项目。 我们将把所有代码写入 programs/src/lib.rs 文件。

配置

打开 programs/src/Cargo.toml 并更新 [features][dependencies] 部分。

[features] 部分中,添加 idl-build 并将其链接到 anchor-langanchor-spl 中的相应子功能。

[dependencies] 部分中,添加 anchor-langanchor-spl。 确保包含 anchor-langinit-if-needed 功能——我们稍后需要它来创建关联 token 账户 (ATA)。

...
[features]
anchor-debug = []
cpi = ["no-entrypoint"]
default = []
**idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]**
no-entrypoint = []
no-log-ix-name = []

custom-heap = []
custom-panic = []
no-idl = []

[dependencies]
**anchor-lang = {version = "0.31.0", features = ["init-if-needed"]}
anchor-spl = {version = "0.31.0"}**

这些依赖项是我们将在本示例中导入和使用的唯一依赖项。

// 从 Anchor 框架和 SPL token 程序导入必要的模块。
use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_2022_extensions::non_transferable_mint_initialize,
    token_interface::{
        mint_to,
        Mint,
        TokenAccount,
        TokenInterface,
    },
};

Mint 设置

为了创建我们的 mint 账户,我们使用 NonTransferable 扩展初始化它。 此扩展仅适用于 mint 本身。 稍后,当通过 Token-2022 ATA 程序为此 mint 创建关联 token 账户时,正如我们之前讨论的那样,ATA 程序会自动将 ImmutableOwnerNonTransferableAccount 扩展添加到新的 token 账户。

下面的代码展示了我们将如何初始化 mint 账户。

  • 它首先使用 NonTransferableMintInitialize 指令初始化 NonTransferable 扩展。
  • 然后,它使用 initialize_mint2 指令,使用有效的授权初始化 mint 账户本身。
// 从 Anchor 框架和 SPL token 程序导入必要的模块。
use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token_2022_extensions::non_transferable_mint_initialize,
    token_interface::{
        mint_to,
        Mint,
        TokenAccount,
        TokenInterface,
    },
};
    /// 使用 NonTransferable 扩展初始化一个新的凭据 mint。
    pub fn initialize_credential_mint(ctx: Context<InitializeCredentialMint>) -> Result<()> {

        // 初始化 NonTransferable 扩展。
        non_transferable_mint_initialize(CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            anchor_spl::token_2022_extensions::NonTransferableMintInitialize {
                mint: ctx.accounts.mint.to_account_info(),
                token_program_id: ctx.accounts.token_program.to_account_info(),
            },
        ))?;

        // 初始化 mint 本身,将小数位数设置为 0,并定义授权。
        anchor_spl::token_interface::initialize_mint2(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token_interface::InitializeMint2 {
                    mint: ctx.accounts.mint.to_account_info(),
                },
            ),
            0, // 小数位数设置为 0,因为凭据是整个单元,不能是小数。
            &ctx.accounts.mint.key(),       // mint 授权是程序派生的地址 (PDA) 本身。
            Some(&ctx.accounts.mint.key()), // 冻结授权也是 PDA。
        )?;

        Ok(())
    }

我们将使用在下面的结构体中定义的账户初始化 mint 账户:

  • 该结构体为 mint 分配 98 字节的空间

    • 8 字节用于账户鉴别器
    • 82 字节用于存储有关 mint 的基本信息,例如其总供应量、小数位数以及谁有权创建更多 token。
    • 另有 8 字节用于 NonTransferable 扩展。

在实践中,我们将使用 ExtensionType::try_calculate_account_len::<PodMint>(&[ExtensionType::NonTransferable])?; 方法动态计算大小。

我在代码中留下了一些注释,以便更好地理解其余部分:

/// 定义 `initialize_credential_mint` 指令所需的帐户。
#[derive(Accounts)]
pub struct InitializeCredentialMint<'info> {
    // 要初始化为程序派生地址 (PDA) 的 mint 账户。
    #[account(\
        init,\
        payer = payer,\
        // 账户数据空间分配:\
        // 8 字节:用于账户鉴别器,Anchor 中账户类型的唯一标识符。\
        // 82 字节:SPL Token Mint 账户的标准固定大小。\
        // 8 字节:为 NonTransferable 扩展保留的额外空间。\
        space = 8 + 82 + 8,\
        owner = token_program.key(),\
        // 定义程序派生地址 (PDA) 的种子。\
        seeds = [b"mint"],\
        bump\
    )]
    pub mint: InterfaceAccount<'info, Mint>,

    // 支付交易和租金的账户。
    #[account(mut)]
    pub payer: Signer<'info>,

    // 系统程序,创建账户所必需。
    pub system_program: Program<'info, System>,
    // SPL token 程序。
    pub token_program: Interface<'info, TokenInterface>,
}

发行凭据

现在我们已经创建了 mint 账户,让我们开始发行凭据。 我们将铸造一个 token (凭据 token) 并将其发送给用户。

    pub fn issue_credential(ctx: Context<IssueCredential>) -> Result<()> {
        // Mint one token to the recipient's associated token account.
        // 将一个 token 铸造到接收者关联的 token 账户。
        mint_to(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token_interface::MintTo {
                    mint: ctx.accounts.mint.to_account_info(),
                    to: ctx.accounts.recipient_ata.to_account_info(),
                    authority: ctx.accounts.authority.to_account_info(),
                },
            ),
            1, // Mint exactly one token.
            // 准确地铸造一个 token。
        )?;

        Ok(())
    }

issue_credential 函数需要 IssueCredential 结构体作为其上下文。当发行凭证时,我们使用 Token-2022 关联的 token 账户 (ATA) 程序创建用户的 token 账户。这会自动将 ImmutableOwner 扩展应用于该账户。由于该 mint 被标记为 NonTransferable,用户的 ATA 也会隐式地应用 NonTransferableAccount 扩展。


/// Defines the accounts required for the `issue_credential` instruction.
/// 定义 `issue_credential` 指令所需的账户。
#[derive(Accounts)]
pub struct IssueCredential<'info> {
    // The mint account, must be mutable.
    // mint 账户,必须是可变的。
    #[account(\
        mut,\
        seeds = [b"mint"],\
        bump,\
        constraint = mint.mint_authority.unwrap() == authority.key() // Ensure the authority is the mint authority.\
        // 确保授权机构是 mint 授权机构。
    )]
    pub mint: InterfaceAccount<'info, Mint>,

    // The authority signing the transaction (must be the mint authority).
    // 签署交易的授权机构(必须是 mint 授权机构)。
    #[account(mut)]
    pub authority: Signer<'info>,

    // The recipient's associated token account, created if it doesn't exist.
    // 接收者的关联 token 账户,如果不存在则创建。
    #[account(\
        init_if_needed,\
        payer = authority,\
        associated_token::mint = mint,\
        associated_token::authority = recipient,\
        associated_token::token_program = token_program\
    )]
    pub recipient_ata: InterfaceAccount<'info, TokenAccount>,

    // The recipient of the credential.
    // 凭证的接收者。
    pub recipient: Signer<'info>,

    // The SPL token program.
    // SPL token 程序。
    pub token_program: Interface<'info, TokenInterface>,
    // The associated token program.
    // 关联的 token 程序。
    pub associated_token_program: Program<'info, AssociatedToken>,
    // The system program.
    // 系统程序。
    pub system_program: Program<'info, System>,
}

因此,有了这个实现,以下操作将是不可能的:

  • 转移凭证 token:转移会失败,出现 TokenError::NonTransferable 错误。我们已经在 Token-2022 程序源代码中看到过这个错误。
  • 更改 token 账户所有者:账户是不可变的,更改会失败

该程序的完整源代码可以在 GitHub 上找到

结论

Token-2022 使得在 Solana 上构建更灵活。我们了解了它的架构以及它如何保持与原始 SPL token 程序的向后兼容性。

我们看到了扩展如何用于向 token 添加新行为。虽然扩展可以一起使用,但其他组合是不允许的。

最后,我们构建了一个凭证发行程序,该程序结合了 NonTransferableImmutableOwner 扩展。这表明在 mint 上启用 NonTransferable 如何防止 token 被转移,从而强制严格控制凭证的持有方式。

本文是Solana 上的教程系列的一部分。

  • 原文链接: rareskills.io/post/token...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/