Token-2022 是 SPL Token 程序的一个向后兼容的新版本,它以扩展(extensions)的形式支持额外的功能。这些扩展的字节码本身就是 Token-2022 程序的一部分,无需部署单独的程序。特定代币所激活的字节码取决于在 mint 或 token 账户中启用的扩展。
例如,在最初的 SPL Token 程序中,添加诸如代币名称、符号或 Logo URL 等元数据需要像 Metaplex 这样的外部程序。但在 Token-2022 中,你可以通过在 token mint 上启用 metadata 扩展来添加元数据。
在本文中,我们将了解 Token-2022 的底层工作原理,以及为了支持扩展所做出的改变。我们将通过一个实际案例,演示如何使用 Token-2022 创建带有扩展的 Solana 代币。
Token-2022 的架构
首先,我们将看看 Token-2022 在底层是如何扩展原始 SPL Token 程序的,账户布局如何保持兼容,以及指令集是如何在不破坏现有程序的情况下进行扩展的。
超集设计与向后兼容性
Token-2022 是 SPL Token 程序的直接替代品(drop-in replacement)。你以前能执行的所有操作——铸造(minting)、转账(transferring)、销毁(burning)、冻结(freezing)、更改权限(changing authorities)——其工作方式仍然完全相同。这是因为 Token-2022 保留了原始 SPL mint 账户的前 82 个字节和 token 账户的前 165 个字节,这些字节包含了如 supply、decimals、owner、freeze authority 和 amount 等字段。以下是原始 SPL mint 账户的布局:

展示 SPL mint 账户前 82 个字节内存布局的图表
原始程序(SPL)仅反序列化并处理这些数据;Token-2022 反序列化相同的区域,然后继续读取该区域之外的数据以获取扩展。
扩展是内置于 Token-2022 程序中的可选功能,你可以在 mint 账户或 token 账户上启用它们。下图展示了 Token-2022 mint 的 TLV 布局:原始 SPL mint 固定的 82 个字节,紧接着是存储扩展数据的可变大小 TLV 区域。

Token-2022 账户布局与 TLV 扩展
原始的 SPL Token 程序具有固定的二进制布局,其中每个字段的大小、类型和值都是预先确定的。Token-2022 在保留的前 82 到 165 字节区域使用相同的固定二进制布局,但在此之外,它使用可变的类型-长度-值(Type-Length-Value,TLV)编码方案来存储扩展。
Type–Length–Value (TLV) 是一种数据序列化方案。数据中的每个对象包含三个部分:
- Type(类型):指定数据代表什么的标识符。
- Length(长度):数据的大小(以字节为单位)。
- Value(值):实际的数据字节。
由于每个条目都声明了它的类型和长度,程序可以读取类型来了解它正在解析的内容,准确读取该长度的字节数作为值,然后使用长度直接跳到下一个条目。
即使某个类型是未知的,程序仍然可以使用长度来跳过它。
例如,在下图中,类型为 12 的条目长度为 2 字节,因此其值字段包含两个字节。如果程序无法识别类型 12,它可以跳过这 2 个字节并直接移至下一个条目,即类型 20。

说明 Type Length Value 的图表
Token-2022 就是使用这种 TLV 编码方案实现的。每个扩展都表示为一个 TLV 条目和以下上下文:
- Type 是扩展的唯一 ID。
- Length 是序列化后的扩展数据的大小(以字节为单位)。
- Value 是序列化后的扩展数据本身。
这种结构允许多个扩展顺序打包到同一个账户中而不会发生冲突。

为了说明 TLV 编码在实践中是如何工作的,我们来看两个扩展:ImmutableOwner 和 MetadataPointer。
ImmutableOwner TLV 布局
ImmutableOwner 扩展是一个 Token-2022 扩展,它防止 token 账户的所有者在创建后被更改。我们现在先了解其 TLV 布局,并在本文后面深入探讨它的用法。
ImmutableOwner 具有以下 TLV 属性:
- Type (T):
0x0a(ImmutableOwner的唯一 ID) - Length (L):
0x01 0x00 0x00 0x00(编码为 4 字节小端整数的数字 1) - Value (V):
0x01(该扩展存储单字节的数据值,为0或1)
其 Rust 定义是一个空结构体:
pub struct ImmutableOwner {}
尽管该结构体没有字段,但 TLV 编码仍然为 V 保留了一个字节来表示该标志是否启用(1 表示启用,0 表示禁用)。这就是该条目具有非零长度的原因。
因此,它的 TLV 布局将如下所示:

MetadataPointer TLV 布局
MetadataPointer 扩展定义了代币链下元数据的位置以及谁可以更新它。
与 ImmutableOwner 扩展不同,MetadataPointer 包含值:两个公钥值(authority 和 metadata_address)。
其 Rust 结构体如下所示:
struct MetadataPointer {
authority: PubKey;
metadata_address: PubKey;
}
在 TLV 中,其布局将包含以下信息:
0x1a:MetadataPointer的 Type ID (T)0x40 0x00 0x00 0x00:Length (L) = 64 字节(小端)<64 bytes>:Value (V),序列化后的authority和指向元数据公开地址的metadata_address。
因为 V 包含一个 authority 和 metadata_address 公钥,所以让我们使用示例的 32 字节公钥来表示它们,以便我们更好地理解其布局是什么样的。
假设账户的 authority 公钥是:
7c4YH58z6Yd1H5pa9vHqPqN8P3f9DuzGcbj2duq5Vn6a
并且 metadata_address 公钥是:
9A4q8Xzj8cQ6w6sKuS27rrR2i1cC6VnV4c7pg1Zg1Vgk
在 Solana 上,公钥是经过 Base58 编码的 32 字节值。值(V)是 authority 和 metadata_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 账户同时初始化了 ImmutableOwner 和 MetadataPointer 扩展,其 TLV 布局将如下所示:

当读取账户时,程序可以遍历 TLV 部分,选择性地仅解码它能够识别的扩展。通过使用它们声明的长度,可以跳过未知的扩展类型。
让我们来看看程序会如何处理一个未知的扩展 UnknownExtension:
- 程序读取
UnknownExtension的 TLV 条目:- T =
0x0b(一个未知类型) - L =
0x14 0x00 0x00 0x00(20,小端) - V =
0x4…0xed(20 字节长,由 L 指定)
- T =
- 由于该类型无法识别,程序不会尝试解析该值
- 但它仍然会读取长度并向前跳过相应的字节数(在这个例子中是 20)
- 然后它会继续处理下一个 TLV 条目(如果有)
现在假设这个未知扩展有 64 字节的值,程序将从 L 中读取值 64,然后向前跳过 64 字节(跨过 V)去寻找下一个 T。这种方法使得 Token-2022 具有向前兼容性;未来的扩展不会破坏现有的程序。
Token-2022 指令兼容性与新功能
Solana 指令是对链上程序的调用,它由三个字段组成:
- Program ID,要调用的链上程序的公钥,
- 账户列表(即程序将读取或写入的 accounts 列表)
- 指令数据(instruction data)—— 由程序定义格式的任意字节序列。
原始的 SPL Token Program 拥有 25 个独特的指令。Token-2022 支持所有这些指令,并在第 25 个指令之后添加了新指令以启用新的扩展功能。
换句话说,现有的代币指令(如 Token-2022 中的 MintTo、Transfer 或 Burn)其行为与 SPL 中完全相同。
以下是一个示例指令布局,它指示 token 程序从 mint 账户向目标 token 账户铸造 100 个代币:

应用程序只需更改其指令中的程序 ID,即可采用 Token-2022。
以下是 Token-2022 中超出原始 Token 程序 25 个指令之外的指令列表。这些 token instructions 的命名与其所初始化或管理的扩展相匹配:
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-2022 中,所有的扩展必须在初始化 mint 或 token 账户之前指定,以便为其数据分配足够的空间。一旦初始化完成,就不能再添加额外的扩展。
你可以使用 spl-token CLI 来创建带有扩展的 Token-2022 代币。它会计算所需的账户大小,将每个扩展的 TLV 条目写入 mint 账户,并最终在后台使用 InitializeMint2 指令初始化 mint。
CLI 模板如下所示:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
create-token <extension flags>
假设我们想要向 mint 添加两个扩展:一个用于定义代币的固定利率(InterestBearingConfig),另一个用于使 mint 能够引用链下元数据(MetadataPointer)。我们可以运行以下命令,附加 —-interest-rate 和 —-enable-metadata 标志。下面命令中的 5 是利率:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
create-token --interest-rate 5 --enable-metadata
此命令创建了一个同时启用了 InterestBearingConfig 和 MetadataPointer 扩展的 mint 账户。在内部,它使用 ExtensionType::try_calculate_account_len::<Mint>(&[InterestBearingConfig, MetadataPointer])?; 分配完整的账户大小,立即写入利率参数(这隐式初始化了 InterestBearingConfig 扩展),并为元数据保留了一个 TLV 槽位。现在,我们必须手动初始化 metadata 扩展。
try_calculate_account_len(来自 token-2022 库) 函数会根据选择的扩展计算所需的总空间,并在最终的 mint 初始化锁定账户结构之前,每个扩展的初始化指令会配置其特定的参数。
如果你运行前面提到的 spl-token 命令来启用扩展,你将看到如下输出:

在此阶段:
- 利率扩展已使用
5的利率完全初始化。 - metadata 扩展预留了一个 TLV 槽位,但尚未写入任何元数据内容。
你会注意到响应中包含此消息:“To initialize metadata inside the mint, please run spl-token initialize-metadata 5bL18vT46c7SkdN37F3pb1GdxsN8kTcZCPoRcYj6cS5w <YOUR_TOKEN_NAME> <YOUR_TOKEN_SYMBOL> <YOUR_TOKEN_URI>, and sign with the mint authority.”
我们将运行该命令来最终完成 metadata 的初始化,并填充已分配的 metadata TLV 块。
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
initialize-metadata 5bL18vT46c7SkdN37F3pb1GdxsN8kTcZCPoRcYj6cS5w \
"MyToken" "MTKN" "https://example.com/mytoken.json"
结果将如下所示:

如果你早些时候省略了 --enable-metadata,这一步将会失败,因为一旦 mint 被初始化,就不能再添加新的 TLV 空间。因此,你会看到如下错误:

向 mint 添加扩展所需的步骤总结如下:
- 通过为扩展分配空间来启用扩展。
- 初始化扩展。你可以一次性初始化多个扩展。
对于像 interest-bearing 扩展这样允许你一次性提供所需参数的扩展(正如我们上面使用 --interest-rate 5 所做的),你将同时启用并初始化该扩展。
以下是一些代币扩展以及你可以使用来在 CLI 上启用它们的标志。
| 扩展 (Extension) | CLI 标志 (CLI Flag) |
|---|---|
| Mint Close Authority | –enable-close |
| Transfer Fees | –transfer-fee-basis-points |
| Non-Transferable | –enable-non-transferable |
| Interest-Bearing | –interest-rate |
| Permanent Delegate | –enable-permanent-delegate |
| Transfer Hook | –transfer-hook |
| Metadata | –enable-metadata |
| Metadata Pointer | –metadata-address |
| Confidential Transfers | –enable-confidential-transfers auto |
Solana Token-2022 源代码 中的 ExtensionType 枚举定义了所有可用的扩展。
组合扩展
你已经看到了我们在创建 mint 账户时是如何组合 Interest Bearing 和 Metadata 扩展的。你也看到了如何启用和初始化这些扩展。
然而,并不是所有的扩展都能与其他扩展组合使用。例如,你不能将 NonTransferable 和 TransferHook 结合使用,因为如果代币不能转账,那么 transfer hook 也就失去了任何实质意义。SVM 和开发工具强制执行了这条规则,因此试图组合这两个扩展将导致执行失败。
此外,你也不能组合 ConfidentialTransfer 和 TransferHook 扩展,因为 confidential transfers 会加密金额并限制可见性。由于 transfer hooks 依赖于读取转账金额,因此两者无法协同工作。
以下是其他一些无法协同工作的扩展组合:
- ConfidentialTransfer 和 TransferFeeConfig
- ConfidentialTransfer 和 PermanentDelegate
总结
在进入本文的下一部分之前,让我们总结一下到目前为止所讨论的内容:
- Token-2022 保留了 Token 账户的前 165 个字节和 Mint 账户的前 82 个字节。
- 扩展使用 TLV(类型-长度-值)编码格式进行存储。
- Token-2022 在原始 SPL Token 程序的 25 个指令基础上增加了 20 个新指令。
- 开发者必须在创建账户时,提前为他们想要启用的所有扩展分配足够的空间。
- 扩展必须在 mint 创建之前初始化。
ImmutableOwner 与 NonTransferable 扩展
我们现在将讨论两个扩展(ImmutableOwner 和 NonTransferable),并演示如何将它们组合起来构建一个凭证发行程序。
ImmutableOwner 扩展
传统的 Token Program 允许你使用 SetAuthority 指令将 Token 账户的 owner 更改为另一个账户。Token-2022 使你能够通过 ImmutableOwner 扩展将 owner 设置为不可更改的状态。ImmutableOwner 扩展会永久锁定 Token 账户的所有权。这意味着,一旦使用该扩展创建了一个 token 账户,它将永久属于指定的钱包。
下面是一个场景,说明了 ImmutableOwner 扩展如何防止传统 Token Program 中可能发生的网络钓鱼攻击(传统程序允许在账户创建后更改所有权)。
在传统 Token Program(没有 ImmutableOwner)中:
- Alice 创建了一个名为
AliceTokenAccount的关联代币账户(associated token account, ATA),该账户由她的钱包地址和特定的 mint 派生而来。 - 她被诱骗签署了一笔交易,将该 ATA 的所有权转移给了 Bob。
- 后来,某个应用程序使用标准的派生函数计算 Alice 的 ATA 地址。该函数依然返回
AliceTokenAccount,因为它不会检查所有权的变更。 - 应用程序向该地址发送代币,以为它属于 Alice。但此时该地址由 Bob 控制,因此 Bob 收到了这些代币。
- 将来任何发送到 Alice 的“ATA”的转账都会进入 Bob 的口袋。
使用 Token-2022 的 ImmutableOwner 扩展(在 ATA 上默认启用):
- 第 2 步中的交易将会失败——所有权无法更改
- 即便 Alice 被诱骗签署恶意交易,她仍保留对其 ATA 的控制权
- 发送到她派生的 ATA 地址的代币将始终能够到达她的账户
以下是 Token-2022 如何利用 ImmutableOwner 扩展来强制执行此约束的
当执行 SetAuthority 指令(在传统 SPL token 程序中用于更改 token 账户所有权的指令)时,它会调用 process_set_authority 函数,该函数会检查当前存在的权限类型。如果权限类型为 AuthorityType::AccountOwner,它将触发 ImmutableOwner 检查,返回错误并阻止所有权变更:

可以在 GitHub 上的 Token-2022 程序核心逻辑中找到这个实现。
对于使用 Token-2022 程序创建的 ATA,ImmutableOwner 扩展默认始终处于启用状态。
当你使用标准 ATA 程序结合 Token-2022 创建 ATA 时,ATA 程序会在账户初始化过程中自动包含 ImmutableOwner 扩展。
无论你是否显式指定它都会发生——这内置于 ATA 程序针对 Token-2022 账户的逻辑中。这有别于通过系统 createAccount 指令手动创建 token 账户,在那种情况下,如果需要,你需要显式地初始化 ImmutableOwner 扩展。
这种设计确保了最常见的代币账户模式(ATA)默认就能获得所有权不可篡改的安全优势。
NonTransferable 扩展
NonTransferable 扩展完全禁用了代币转账。你仍然可以将代币铸造到账户中,但一旦收到,就无法再将其发送到其他任何地方。
对这类账户唯一有效的操作是销毁代币(对于有 Ethereum 背景的人来说,Solana 将代币销毁视为与转账独立的操作)或关闭空账户。
这对于构建代币代表不可转让资产的系统非常有用。例如,证书或链上身份标记不应该在用户之间移动,用户欠某个协议的债务也是如此。在这些情况下,你可以发行带有 NonTransferable 扩展的 NFT。
Token-2022 在程序逻辑中强制执行这一限制。如果你尝试从持有不可转让代币的账户进行转账,该指令将失败并报错。
你可以在 processor.rs 中看到此检查,在该文件中,程序会检查账户是否具有 NonTransferableAccount 扩展,如果存在,则会因 NonTransferable 错误而拒绝转账。

不可转让代币只能铸造到具有 immutable owner 的账户中。这是为了防止通过账户所有权变更来进行间接的代币转账。
当使用 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());
}
示例:使用 ImmutableOwner 和 NonTransferable 扩展构建一个最小的凭证发行程序
本节将涵盖:
- 如何定义同时具有
ImmutableOwner和NonTransferable扩展的 mint。 - 如何向接收者发放不可转让的凭证代币
- 以及 Token-2022 程序逻辑如何确保这些凭证不能被转移
免责声明:此处描述的 Anchor 程序仅供教育目的使用。在生产环境中,请确保你的程序包含完整的验证、安全检查,并经过代码审查。
项目设置
通过运行命令 anchor init credentials 创建一个新的 Anchor 项目。我们将把所有的代码编写在 programs/src/lib.rs 文件中。
配置
打开 programs/src/Cargo.toml 并更新 [features] 和 [dependencies] 部分。
在 [features] 部分,添加 idl-build 并将其链接到 anchor-lang 和 anchor-spl 中相应的子特性。
在 [dependencies] 部分,添加 anchor-lang 和 anchor-spl。确保为 anchor-lang 包含 init-if-needed 特性——我们稍后将需要它来创建关联代币账户(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"}**
这些依赖项是我们在整个示例中唯一会导入和使用的。
// Import necessary modules from the Anchor framework and SPL token program.
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 创建关联代币账户时,ATA 程序会自动向新的 token 账户中添加 ImmutableOwner 和 NonTransferableAccount 扩展,正如我们之前所讨论的那样。
下面的代码展示了我们将如何初始化 mint 账户。
- 它首先使用
NonTransferableMintInitialize指令初始化NonTransferable扩展。 - 然后,它使用
initialize_mint2指令利用有效的权限初始化 mint 账户本身。
// Import necessary modules from the Anchor framework and SPL token program.
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token_2022_extensions::non_transferable_mint_initialize,
token_interface::{
mint_to,
Mint,
TokenAccount,
TokenInterface,
},
};
/// Initializes a new mint for the credentials
/// with NonTransferable extension.
pub fn initialize_credential_mint(ctx: Context<InitializeCredentialMint>) -> Result<()> {
// Initialize the NonTransferable extension.
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(),
},
))?;
// Initialize the mint itself, setting decimals to 0 and defining authorities.
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, // Decimals are set to 0 because credentials are whole units and cannot be fractional.
&ctx.accounts.mint.key(), // The mint authority is the program-derived address (PDA) itself.
Some(&ctx.accounts.mint.key()), // The freeze authority is also the PDA.
)?;
Ok(())
}
我们将使用下方结构体中定义的账户来初始化 mint 账户:
-
该结构体为 mint 分配了 98 个字节的空间
- 8 字节用于 account discriminator
- 82 字节用于存储关于 mint 的基本信息,例如其总供应量、小数位数以及谁有权创建更多的代币。
- 另外 8 字节用于
NonTransferable扩展。
在实践中,我们将使用
ExtensionType::try_calculate_account_len::<PodMint>(&[ExtensionType::NonTransferable])?;方法来动态计算大小。
我在代码中留下了一些注释,以便更好地理解其余部分:
/// Defines the accounts required for the `initialize_credential_mint` instruction.
#[derive(Accounts)]
pub struct InitializeCredentialMint<'info> {
// The mint account to be initialized as a Program Derived Address (PDA).
#[account(
init,
payer = payer,
// The space allocation for the account's data:
// 8 bytes: for the account discriminator, a unique identifier for the account type in Anchor.
// 82 bytes: the standard fixed size of a SPL Token Mint account.
// 8 bytes: additional space reserved for the NonTransferable extension.
space = 8 + 82 + 8,
owner = token_program.key(),
// Defines the seeds for the Program Derived Address (PDA).
seeds = [b"mint"],
bump
)]
pub mint: InterfaceAccount<'info, Mint>,
// The account paying for the transaction and rent.
#[account(mut)]
pub payer: Signer<'info>,
// System program, required for creating accounts.
pub system_program: Program<'info, System>,
// The SPL token program.
pub token_program: Interface<'info, TokenInterface>,
}
发放凭证
既然我们已经创建了 mint 账户,让我们开始发放凭证。我们将铸造正好一个代币(凭证代币)并将其发送给用户。
pub fn issue_credential(ctx: Context<IssueCredential>) -> Result<()> {
// Mint one token to the recipient's associated token account.
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.
)?;
Ok(())
}
issue_credential 函数期望使用 IssueCredential 结构体作为其上下文。在发放凭证时,我们使用 Token-2022 关联代币账户(ATA)程序创建用户的 token 账户。这会自动将 ImmutableOwner 扩展应用于该账户。由于该 mint 被标记为 NonTransferable,用户的 ATA 也将隐式应用 NonTransferableAccount 扩展。
/// Defines the accounts required for the `issue_credential` instruction.
#[derive(Accounts)]
pub struct IssueCredential<'info> {
// The mint account, must be mutable.
#[account(
mut,
seeds = [b"mint"],
bump,
constraint = mint.mint_authority.unwrap() == authority.key() // Ensure the authority is the mint authority.
)]
pub mint: InterfaceAccount<'info, Mint>,
// The authority signing the transaction (must be the mint authority).
#[account(mut)]
pub authority: Signer<'info>,
// The recipient's associated token account, created if it doesn't exist.
#[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.
pub token_program: Interface<'info, TokenInterface>,
// The associated token program.
pub associated_token_program: Program<'info, AssociatedToken>,
// The system program.
pub system_program: Program<'info, System>,
}
因此,通过这个实现,将不可能出现以下情况:
- 转移凭证代币:转账将失败并抛出
TokenError::NonTransferable。我们之前已经在 Token-2022 程序源代码中看到了这个错误。 - 更改 token 账户的所有者:该账户是不可改变的,任何更改都会失败
此程序的完整源代码可以在 GitHub 上找到。
结论
Token-2022 使得在 Solana 上的开发构建变得更加灵活。我们了解了 token-2022 的架构,以及它如何与原始的 SPL token 程序保持向后兼容性。
我们看到了如何使用扩展来为代币添加新的行为。虽然有些扩展可以组合使用,但某些特定的组合是不被允许的。
最后,我们构建了一个结合了 NonTransferable 和 ImmutableOwner 扩展的凭证发行程序。这展示了在 mint 上启用 NonTransferable 如何防止代币被转账,从而对凭证的持有方式实施严格的控制。
本文是 Solana 系列教程的一部分。