Solana Program Library Token (SPL Token) 是 Solana 的代币标准:定义了如何创建代币以及它们应有的行为。它相当于 Ethereum 的 ERC-20(同质化代币)和 ERC-721(NFT)等代币标准。
与 Ethereum 为每个代币标准使用单独的智能合约不同,Solana 上的所有 SPL 代币都使用同一个程序 (program)。这意味着 Solana 上的所有代币共享相同的底层逻辑,代币特定的参数是在创建时设置的,而不是通过不同的程序代码来定义。SPL Token 程序仅包含逻辑,而所有代币数据则单独存储。这与 Solana 将逻辑和状态分离到独立账户中的方式是一致的。
可以将 SPL 代币与 Ethereum 进行对比来理解:在 Ethereum 上,通常需要为每个独特的代币部署一个新的智能合约(如 ERC-20)。在 Solana 上,你不需要部署新代码,而是与这单一的 SPL 程序进行交互,该程序包含了定义代币、铸造、转账、授权和销毁所需的所有指令 (instructions)。
在 Ethereum 上,每个代币都是一个带有自定义代码的独立智能合约,这意味着 USDC 处理授权的方式可能与 DAI 不同;这在灵活性方面具有优势,但也可能导致意外行为。在 Solana 上,每个 SPL 代币都使用相同的转账机制、相同的授权系统和相同的安全检查。
本文将解释 SPL 代币的概念,以及 Solana 如何将代币逻辑与代币数据分离。内容涵盖:
- Solana 的代币架构与 Ethereum 有何不同,
- 使 SPL 代币运作的三个关键账户,
- 为什么 Solana 对所有代币使用同一个程序,以及
- Solana 如何追踪用户的代币余额。
在下一篇文章 Creating SPL Tokens with Anchor 中,我们将展示如何创建和转账 SPL 代币。
为了理解这一切在实践中是如何运作的,我们首先来看看 SPL Token 程序本身。有时我们会直接简称它为 token program,两者指的是同一个东西。
Token Program
SPL Token 程序是负责管理 SPL 代币功能的核心链上程序。它包含创建 SPL 代币的逻辑,并定义了它们的行为。SPL Token 程序位于一个固定地址:TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA。
Token Program 拥有存储 SPL 代币状态的所有账户(我们将随着文章的深入介绍这些账户)。这种所有权意味着 Token Program 是唯一能够修改这些账户中数据的程序。
需要温习 Solana 账户所有权模型的读者,请参阅 Understanding Account Ownership in Solana。
接下来,我们将讨论与 SPL 代币相关的不同账户,即 Mint Account、Token Account 和 Associated Token Account (ATA)。每个账户在代币记账和转账中都扮演着特定的角色。
Mint account
每个独立的 SPL 代币都有一个唯一的 Mint account,用于存储有关该代币的全局信息。它保存了代币的总供应量、精度(小数位数)以及哪些地址(如果有的话)拥有铸造代币和冻结账户(即黑名单)的权限 (authority) 等数据。如前所述,代币的核心逻辑仍保留在 SPL Token Program 中。
每个 Mint account 都是唯一的,并在初始化新的 SPL 代币时通过 SPL Token Program 创建。当我们在 Solana 中提到代币地址时,指的也是它的 Mint account 地址,因为它们是同一个概念。例如,以下分别是 USDC 和 USDT 的代币地址 (mint accounts):EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 和 Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB。


下图展示了 Token program 和 mint accounts 之间的关系。

Mint account details
与所有 Solana 账户一样,mint account 具有标准的元数据字段,分别是:
- Lamports(用于租金)
- Owner(在此情况下是 Token Program 地址)
- Executable status(值为
false,因为它仅用于数据存储)
有关 Solana 账户的更多信息,请参阅 Initializing Accounts in Solana and Anchor。
除了这些标准字段外,mint account 还包含特定的数据字段,用于定义代币本身:
mint_authority:允许创建新代币的地址。freeze_authority:允许冻结持有该代币的 token accounts 的地址。decimals:该代币使用的小数位数(0-9)。supply:已创建的代币总数。is_initialized:防止重复初始化的布尔标志。
虽然 mint account 存储了代币信息,但它并不追踪单个用户的余额,这由我们稍后将讨论的独立账户来处理。
下图展示了 mint account 的属性。

Mint account 的铸造和冻结权限 (authorities)(如上图所示)是在创建期间分配的,通常设置为交易签名者的地址。我们将在“Token Program Instructions”部分看到相关的指令。
Mint accounts 的一个重要方面是它们如何控制代币的供应量。这是通过 mint authority 字段来处理的。我们将在下面进行解释。
How to Set a Maximum Token Supply
SPL 代币通过移除权限而不是显式限制来实现最大供应量。这种设计选择源于 Solana 和 Ethereum 处理状态的根本差异。
Mint account 数据中没有显式的“最大供应量”字段。因此,为了创建固定供应量,你需要通过将 mint authority 设置为 None(空值)来禁用它。这将永久禁用该铸币权限,因为它不再分配给任何人。由于没有账户拥有铸造更多代币的权限,当前的总供应量就成为了固定的最大值。
在 Ethereum 中,限制代币总供应量的传统方法是将数字显式存储在某个地方,并阻止超过该数字的铸造行为。SPL 代币没有“总供应量上限”的概念,因此不会在任何地方存储这样的数字。
在使用 SPL 时,如果我们想要 100 万个代币的总供应量,我们会将所有 100 万个代币铸造给持有者,然后禁用 mint authority。或者,正如我们在随后的教程中将看到的,我们也可以让一个单独的程序成为 mint authority,并让该程序在达到供应量上限后停止铸造代币。
Token Accounts and Associated Token Accounts (ATAs)
如前所述,mint account 仅存储有关代币本身的信息。为了追踪单个用户的余额,Solana 使用了称为 Token Accounts 的独立账户。
Token Accounts
Token Accounts 是 Solana 账户,用于存储用户的代币余额、该账户关联的 mint 地址、可以授权转账的 owner,以及我们稍后将详细介绍的其他字段。
根据设计,用户可以为同一种代币拥有多个创建在不同地址的 Token Accounts。这带来了一个挑战,正如 Solana Program Library 文档中所述:
“用户可能拥有任意数量属于同一个 mint 的 token accounts,这使得其他用户很难知道应该将代币发送到哪个账户。”
这意味着用户某种代币的余额可能会分散在几个账户中,而不是集中在一个地方。
例如:
- Alice 在一个 token account 中有 5 个代币,在另一个 token account 中有 15 个代币。这两个 token accounts 都属于同一个 mint,因此她总共拥有该 mint 的 20 个代币。
- 然而,如果 Bob 想要向 Alice 发送更多代币,他很难轻易知道 Alice 更希望用哪个账户来接收。
这就是 SPL 文档中提到的“任意数量代币账户”带来的挑战。这些余额并不是冗余副本,而是分散在多个账户中的总余额的组成部分。
为了解决这个问题,Solana 引入了 Associated Token Accounts (ATAs)。
Associated Token Accounts (ATAs)
与常规的 Token Accounts(用户可以为每个 mint 拥有多个账户)不同,ATAs 是一种特殊的 Token Accounts(具有查找地址的确定性规则),它强制用户钱包地址和 mint 之间保持一对一的关系。这确保了:
- 每个用户针对每种代币类型都有且仅有一个可预测的 ATA
- 任何应用程序都可以无需先验知识轻松找到用户的代币余额,因为该地址是确定性的。我们将解释如下。
本文主要关注 ATAs,因为正如上文讨论的常规 token addresses 所面临的挑战,ATAs 已经成为在 Solana 中管理代币的标准方法。
How are ATA addresses derived?
ATA 地址是一个 Program Derived Address (PDA),它由两个输入确定性地推导出来:
- 用户/签名者的钱包地址(预期的 authority)。
- 代币 mint account 的地址。
我们可以将 Associated Token Account 与 Ethereum ERC-20 中的 mapping(address => uint256) public balanceOf 进行比较,因为两者都是追踪用户拥有多少代币的方法。
因为一个用户可以拥有多种 SPL 代币,仅仅使用用户的地址作为键(key)不足以区分不同代币的余额。这就是为什么 mint address 也要包含在推导过程中的原因。通过结合用户的钱包地址和代币的 mint address,Solana 确保了每个(用户,代币)组合都能获得一个唯一的 ATA 地址。
这种设计避免了冲突并强制实现了一致的结构:
user_wallet_address + token_mint_address => associated_token_account_address
为了让这一点更清晰,下表对比了 Ethereum 和 Solana 是如何管理代币余额的。
| 对比维度 | Ethereum (ERC-20) | Solana (ATAs) |
|---|---|---|
| 存储模型 | 一个核心合约在 mapping 中存储所有余额 | 每个用户针对每种代币都有一个单独的账户 (ATA) |
| 余额位置 | 存储在代币合约内部 | 存储在用户的 ATA 内部 |
| 谁支付存储费用 | 合约所有者(部署成本) | 用户为其账户付费 |
| 查找方式 | 调用 balanceOf(user) |
推导 ATA 地址 → 读取余额 |
| 并行访问 | 受到合约限制 | 完全并行 |
两者都实现了相同的目标——追踪代币所有权——但 Solana 的方法能够实现并行处理,因为每个余额都在一个独立的账户中。
下图展示了 Associated Token Account 的 字段 (fields)。

ATA 保存了用户针对特定代币/mint 的余额详情。其关键字段包括:
mint:该账户持有的代币(Mint Account)地址。例如,如果它代表 USDC 余额,那么这就是 USDC mint account 的地址。owner:虽然标记为‘owner’,但它实际上是 ATA 的权限控制者 (authority)。由于强制执行所有规则,每个 ATA 真正的 owner 始终是 Token Program。此处的owner字段用于告诉 Token Program,必须由哪个 authority 签名才能通过更新或转账。回顾在 Owner vs Authority 一文中的内容,账户的 owner 负责执行其规则,而 authority 是唯一能发送指令修改该账户的有效签名者(除非该 authority 已通过 Token Program 委派了签名权)。amount:此余额中持有的代币数量。delegate:已被授权转账的 delegate 账户的地址。由于只有一个delegate字段,一个 token account 在同一时间只能存在一个 delegate。这与 ERC-20 不同,在 ERC-20 中一个 owner 可以授权多个 spenders。state:token account 的状态。这是一个 枚举 (enum),状态可以是Uninitialized、Initialized或Frozen。close_authority:被允许关闭该账户的地址。默认情况下与owner是同一个公钥,但owner也可以指定另一个close_authority。当一个 token account 的余额归零时,owner 可以关闭它以收回用于免租的 SOL。一些网络工具如 Solflare wallet 和 Sol-Incinerator 提供了关闭空代币账户的便捷方法。
下图展示了 Token Program、mint account 和 token account 之间的关系。

(从这里开始,当我们提到“token account”时,也包括 ATAs,因为 ATAs 只是一种特殊类型的 token account。)
Associated Token Account Program
Associated Token Account 程序有一个固定的地址 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL。它是一个链上程序,用于查找或为给定的 用户-代币 对创建正确的 ATA。它负责确定性地推导地址,并在需要时通过 跨程序调用 (Cross Program Invocation, CPI) 在 Token Program 中创建新的 ATAs。
具体来说,由 ATA Program 编排的创建流程如下:

与 Ethereum 中余额隐式存在于合约存储中不同,Solana 要求显式创建账户。这就带来了一个根本性的用户体验 (UX) 挑战:由于代币余额实质上就存储在 Associated Token Account 中,因此无法将代币发送给尚未显式创建接收端 Associated Token Account 的用户。
因此,在发送任何代币之前,我们必须为 用户-代币 对创建 ATA。ATA 地址是在链下使用钱包地址和 mint address 确定性推导出来的。推导完成后,如果该 ATA 在链上还不存在,我们会使用 Associated Token Account Program 来创建它。这引出了一个重要的安全问题:如果任何人都可以为别人创建 ATA,他们是否也能将自己指定为 ATA 的 owner 和 close_authority?幸运的是,答案是否定的。当为另一个钱包创建 ATA 时,ATA Program 会强制将 owner 和 close_authority 字段始终设置为该 ATA 所属的钱包地址,而不是交易签名者。这种安全保证内置于 ATA Program 的代码 中,确保只有合法的钱包所有者才能保持对其代币的控制权以及关闭其账户的能力。
举例说明:当 Alice 想向 Bob 发送代币时,她会推导出 Bob 的 ATA 地址,如果链上不存在则创建它,然后将 Bob 的 ATA 作为目的地调用 Token Program 的 Transfer 指令。(在实践中,像 @solana/spl-token 这样的客户端库提供了将 ATA 推导和创建步骤结合在一起的辅助函数)。举例说明:当 Alice 想向 Bob 发送代币时,她会推导出 Bob 的 ATA 地址,如果链上不存在则创建它,然后将 Bob 的 ATA 作为目的地调用 Token Program 的 Transfer 指令。(在实践中,像 @solana/spl-token 这样的客户端库提供了将 ATA 推导和创建步骤结合在一起的辅助函数)。
下图展示了 ATA program 的内容。

我们已经讨论了参与创建和管理 SPL 代币的账户。接下来,我们将介绍 Token Program 和 ATA program 的指令 (instructions)。这些指令让你能够执行诸如创建和铸造新代币、在 ATAs 之间发送代币、设置他人花费你代币的授权、销毁代币以减少供应量以及关闭空账户以回收租金等操作。
Token Program Instructions
让我们探讨一下 Token Program 提供的公共函数,它们允许你与 SPL 代币进行交互。
请注意,在下面的指令参数中,当我们提到 token accounts 时,它们既可以是常规的 token accounts,也可以是 Associated Token Accounts (ATAs),正如我们前面在 Token Accounts 与 Associated Token Accounts 部分所讨论的。当区分这两者很重要时,我们会明确指定是指常规 token accounts 还是 ATAs。
Token Program 具有以下公共函数:
InitializeMint:此指令创建一个新的 mint account,代表链上的一个新的 SPL 代币。
pub fn initialize_mint(
mint_pubkey: &Pubkey, // The mint account to initialize
decimals: u8, // The number of decimal places for the token
mint_authority: &Pubkey, // The account with permission to create new tokens
freeze_authority: Option<&Pubkey> // Optional: The account that can freeze token accounts
) -> Instruction
mint_pubkey 可以是一个未使用的 keypair 账户或 PDA 的地址,只要它打算被初始化为一个 mint account。我们将在下一个教程的实践中看到这一过程。
InitializeAccount:此指令初始化一个新的常规 token account(非 ATA),以保存用户特定 SPL 代币 mint 的余额。
pub fn initialize_account(
account_pubkey: &Pubkey, // The token account to initialize
mint_pubkey: &Pubkey, // The mint for the new token account
owner_pubkey: &Pubkey // The owner of the new token account
) -> Instruction
ATAs 是由 ATA Program 初始化的,它在底层对该 InitializeAccount 指令执行 CPI。稍后我们会看到具体是如何实现的。
Transfer:用于将 SPL 代币单位从一个用户的 token account(源头)转移到另一个用户的 token account(目的地)。“余额”只是存储在 associated token account 中的一个数字,只有 SPL 程序可以修改它。请注意,mint account 和 token account 必须已经存在,或者在调用 MintTo 指令之前刚刚被创建(这同样适用于下面的 MintTo 指令)。我们将在下一个教程中使用 Anchor 来演示这一点。
pub fn transfer(
source_pubkey: &Pubkey, // The token account sending tokens (typically the sender's ATA; not the mint account)
destination_pubkey: &Pubkey, // The destination token account (to which tokens are received)
authority_pubkey: &Pubkey, // The owner or delegate authorized to spend from the sending token account
amount: u64 // The number of tokens to transfer
) -> Instruction
MintTo:此指令创建新的代币单位,并将它们添加到指定的 token account 中。
pub fn mint_to(
mint_pubkey: &Pubkey, // The token mint address
account_pubkey: &Pubkey, // The token account to mint to
authority_pubkey: &Pubkey, // The mint's minting authority
amount: u64 // The amount to mint
) -> Instruction
Burn:此指令从 token account 中销毁指定数量的 SPL 代币单位,从而减少代币总供应量。这与 ERC-20 的 burn 函数工作原理类似。
pub fn burn(
account_pubkey: &Pubkey, // The token account to burn from
mint_pubkey: &Pubkey, // The token mint
authority_pubkey: &Pubkey, // The token account's owner/delegate
amount: u64 // The amount to burn
) -> Instruction
Approve:此指令将花费权限从 token account 的 owner 委托给指定的 delegate,并设定最大金额。它会在该 token account 上设置 delegate 字段以及一个授权额度;同一时间只能存在一个 delegate。在该额度限制内,delegate 可以代表 owner 转账代币。
与将授权额度存储在合约 mapping 中的 ERC-20 不同,SPL 直接在 owner 的 token account(通常是 ATA)上记录审批授权。这种设计使得审批和转账可以在单笔交易中完成,因为仅修改了 token account 的状态。
pub fn approve(
source_pubkey: &Pubkey, // The token account granting approval
delegate_pubkey: &Pubkey, // The delegate account
owner_pubkey: &Pubkey, // The owner of the token account granting approval
amount: u64 // The maximum number of tokens the delegate can transfer
) -> Instruction
Revoke:此指令取消任何先前授予的委托审批(通过 Approve 指令完成的)。它通过将 token account 的 delegate 字段设置为 None(无 delegate)来完全移除该 delegate。
由于审批授权无法部分减少,如果你想降低授权额度,就必须使用较小的金额设置一个新的审批,这类似于 ERC-20 减少授权额度的方式。
pub fn revoke(
source_pubkey: &Pubkey, // The token account revoking approval (same account that previously granted approval)
owner_pubkey: &Pubkey // The owner of the token account revoking approval
) -> Instruction
FreezeAccount:此指令用于冻结 token account,在解冻之前暂时阻止任何涉及该账户所持代币的转账或交易。换句话说,SPL 支持将用户的 token account 地址加入黑名单。
pub fn freeze_account(
account_pubkey: &Pubkey, // The token account to freeze
mint_pubkey: &Pubkey, // The token mint
authority_pubkey: &Pubkey // The mint's freeze authority
) -> Instruction
ThawAccount:此指令解冻先前被冻结的 token account,从而允许恢复代币转账和交易。
pub fn thaw_account(
account_pubkey: &Pubkey, // The token account to unfreeze
mint_pubkey: &Pubkey, // The token mint
authority_pubkey: &Pubkey // The mint's freeze authority
) -> Instruction
SetAuthority:此指令更改谁在 mint accounts 和 token accounts 上持有特定的 authority 角色。
回想一下,mint account 有两个带有“authority”的字段:
mint_authorityfreeze_authority
(关联的)token account 也有两个带有“authority”的字段:
owner:代币的所有者,而不是 PDA 的“Solana runtime owner”(这种命名很容易混淆)。delegate:一个能够代表 owner 花费代币的公钥。
下面 set_authority 中的 account_pubkey 既可以指 mint account,也可以指 token account。
指定的 authority_type 必须与该账户持有的 authority 类型相匹配。
Solana 的 SPL 源代码 为这四种 authority 给出了相应的 enum 名称:
MintTokensFreezeAccountAccountOwnerCloseAccount
请注意,SPL 程序在 token account 中引用 authority 角色的方式并不一致,并且令人困惑地将代币的 owner 称为“owner”——不应将其与 PDA 的 owner 混淆。
pub fn set_authority(
account_pubkey: &Pubkey, // The mint or token account
current_authority_pubkey: &Pubkey, // The current authority
authority_type: AuthorityType, // The type of authority to change (e.g., MintTokens, FreezeAccount)
new_authority_pubkey: Option<&Pubkey> // The new authority, or None to disable
) -> Instruction
Revoke 与 SetAuthority 的效果相同,两者都会更改由谁来持有某个 authority。Revoke 会清除 token account 的委托(将 delegate 设置为 None),而 SetAuthority 会更改 mint/account 的 authorities(即 MintTokens、FreezeAccount、AccountOwner、CloseAccount)。
CloseAccount:此指令永久关闭一个 associated token account,并回收该账户用于免租的 SOL lamport 余额。然而,该 ATA 中的底层 mint 代币余额必须严格为零,否则将返回错误。
pub fn close_account(
account_pubkey: &Pubkey, // The account to close
destination_pubkey: &Pubkey, // The account to receive the reclaimed SOL
owner_pubkey: &Pubkey // The closing account's owner
) -> Instruction
现在,我们来讨论 ATA program 的关键指令。
Associated Token Account (ATA) Program Instructions
ATA program 协同 Token Program 一起工作,具有以下主要指令:
Create:此指令在一个由钱包地址和代币 mint address 组合推导出的确定性 PDA 地址处创建一个 ATA。如果推导出的地址处已经存在一个账户,该指令将失败报错。
pub fn create_associated_token_account(
payer: &Pubkey, // The account funding the creation
wallet_address: &Pubkey, // The wallet address for the ATA
token_mint: &Pubkey // The token mint
) -> Instruction
CreateIdempotent:确保在推导出的 PDA 地址处存在正确的 ATA。如果需要,它会创建该账户。但与 Create 指令不同的是,即使正确的账户已经存在,它也会成功执行而不会报错。
pub fn create_associated_token_account_idempotent(
payer: &Pubkey, // The account funding the creation
wallet_address: &Pubkey, // The wallet address for the ATA
token_mint: &Pubkey // The token mint
) -> Instruction
Create 和 CreateIdempotent 都会推导 ATA 地址,然后对 Token Program 的 InitializeAccount 指令(我们之前见过的)执行 CPI,从而设置 associated token account。
总结
综上所述,Solana 的 SPL 代币架构建立在程序逻辑与代币数据根本分离的基础之上。在 Solana 上,所有代币均由同一个核心的 Token Program 管理,而不是像在 Ethereum 上那样为每个代币部署一个新的合约。
以下是需要记住的最重要几点:
- 逻辑与状态: 单一的 Token Program 包含了所有的规则(如转账、铸造、销毁),但它本身不保存任何代币数据(如余额、精度、供应量)。它充当了所有 SPL 代币的通用逻辑引擎。
- Mint Account 即代币: Mint Account 定义了唯一的代币。它存储了该代币的全局信息,比如总供应量、精度以及谁拥有继续创建的权限。Mint account 的地址就是代币的地址(例如 USDC 的 mint 地址)。
- 余额存放在 Token Accounts/ATAs 中: 用户的余额保存在独立的 Token Accounts 或 Associated Token Accounts (ATAs) 中。不是将地址和余额映射在一个大合约中,而是每个用户都会为他们拥有的每种代币类型获取一个独特的账户。ATA 的地址是通过用户的钱包和代币的 mint address 以可预测的方式推导出来的,并且是推荐的解决方案。
SPL 架构的一些优势
- 并行处理: 因为每个用户的余额都在一个独立的账户中,网络可以同时处理数以千计的转账,而它们之间不会相互干扰。
- 标准化: 每一个 SPL 代币,无论是稳定币还是模因币 (meme coin),都遵循来自核心 Token Program 的完全相同且安全的逻辑。这降低了自定义代币合约可能出现的错误风险。
本文是 Solana 教程系列 的一部分。