本文介绍了Solana的SPL Token标准,它类似于以太坊的ERC-20和ERC-721标准。
Solana Program Library Token (SPL Token) 是 Solana 的代币标准:如何创建代币以及它们应该如何运作。它相当于 Solana 的以太坊代币标准,如 ERC-20(同质化代币)和 ERC-721 (NFTs)。
与以太坊为每个代币标准使用单独的智能合约不同,Solana 上的所有 SPL 代币都使用相同的程序。这意味着 Solana 上的所有代币都共享相同的底层逻辑,代币特定的参数在创建期间设置,而不是通过不同的程序代码设置。SPL Token 程序仅包含逻辑,而所有代币数据都单独存储。这与 Solana 如何将逻辑与状态分离到不同的帐户中是一致的。
以下是一种思考 SPL 代币与以太坊对比的方式:在以太坊上,你通常为每个独特的代币部署一个新的智能合约(如 ERC-20)。在 Solana 上,你不是部署新代码,而是与这个单一的 SPL 程序交互,该程序包含定义代币、铸造、转移、批准和销毁所需的所有指令。
在以太坊上,每个代币都是具有自定义代码的独立智能合约,这意味着 USDC 处理批准的方式可能与 DAI 不同;这在灵活性方面具有优势,但也可能导致意想不到的行为。在 Solana 上,每个 SPL 代币都使用相同的转移函数、相同的批准系统和相同的安全检查。
本文解释了 SPL 代币的概念,以及 Solana 如何将代币逻辑与代币数据分离。它涵盖:
在下一篇文章,使用 Anchor 创建 SPL 代币中,我们将展示如何创建和转移 SPL 代币。
为了理解所有这些如何在实践中工作,我们首先来看 SPL Token 程序本身。我们有时会简单地称它为代币程序,但两者意思相同。
SPL Token 程序是负责管理 SPL 代币功能的核心链上程序。它包含创建 SPL 代币的逻辑,并定义它们如何运作。SPL Token 程序位于一个固定的地址:TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA。
代币程序拥有所有存储 SPL 代币状态的帐户(我们将在进展过程中介绍这些帐户)。这种所有权意味着代币程序是唯一可以修改这些帐户中数据的程序。
对于需要复习 Solana 帐户所有权模型的读者,请参阅 理解 Solana 中的帐户所有权。
接下来,我们将讨论与 SPL 代币相关的不同帐户,分别是 Mint 帐户,以及 Token 帐户和关联 Token 帐户 (ATA)。每个帐户在核算和转移代币方面都起着特定的作用。
每个单独的 SPL 代币都有一个唯一的 Mint 帐户,该帐户存储关于该代币的全局信息。它保存着诸如代币的总供应量、小数位数以及哪些地址(如果有)有权铸造代币和冻结帐户(即列入黑名单)等数据。如上所述,代币的核心逻辑保留在 SPL Token 程序中。
每个 mint 帐户都是唯一的,并且在初始化新的 SPL 代币时通过 SPL Token 程序创建。当我们在 Solana 中引用代币地址时,我们也指的是它的 mint 帐户地址,因为它们是相同的。例如,以下分别是 USDC 和 USDT 的代币地址(mint 帐户): EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 和 Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB。


下图显示了 Token 程序和 mint 帐户之间的关系。

像所有 Solana 帐户一样,mint 帐户具有标准的元数据字段,分别是:
false,因为它用于数据存储)有关 Solana 帐户的更多信息,请参阅 在 Solana 和 Anchor 中初始化帐户。
除了这些标准字段外,mint 帐户还包含特定的数据字段,这些字段定义了代币本身:
mint_authority:允许创建新代币的地址。freeze_authority:允许冻结持有此代币的代币帐户的地址。decimals:代币使用的小数位数 (0-9)。supply:已创建的代币总数。is_initialized:防止重新初始化的布尔标志。虽然 mint 帐户存储代币信息,但它不跟踪个人用户余额,这由我们稍后将讨论的单独帐户处理。
下图显示了 mint 帐户属性。

mint 帐户的 mint 和 freeze 权限(如上所示)在创建期间分配,通常设置为交易签署人的地址。我们将在“代币程序指令”部分看到此指令。
mint 帐户的一个重要方面是它们如何控制代币的供应。这通过 mint 权限字段处理。我们在下面对此进行解释。
SPL 代币通过权限移除而不是显式限制来实现最大供应量。这种设计选择源于 Solana 和以太坊处理状态方式的根本差异。
mint 帐户数据没有显式的“最大供应量”字段。因此,要创建固定供应量,你可以通过将其设置为 None(nil 值)来禁用 mint 权限。这会永久地 禁用 mint 权限,因为它将其分配给任何人。由于没有帐户持有铸造更多代币的权限,因此当前的总供应量变为固定的最大值。
在以太坊中,限制代币总供应量的传统方法是显式地将该数字存储在某个地方,并阻止超过此数字的铸造。SPL 代币没有“总供应量限制”的概念,因此不会在任何地方存储这样的数字。
对于 SPL,如果我们想要总供应量为 100 万个代币,我们会将所有 100 万个代币铸造给持有者,然后禁用 mint 权限。或者,正如我们在稍后的教程中将看到的,我们也可以使一个单独的程序成为 mint 权限,并让该程序在达到供应上限后停止铸造代币。
如前所述,mint 帐户仅存储有关代币本身的信息。为了跟踪个人用户余额,Solana 使用单独的帐户,称为 Token 帐户。
Token 帐户是 Solana 帐户,用于存储用户的代币余额、帐户关联的 mint 地址以及可以授权转帐的所有者,以及我们稍后将详细介绍的其他字段。
按照设计,用户可以拥有同一代币的多个 Token 帐户,这些帐户在不同的地址创建。正如 Solana 程序库文档所述,这会带来挑战:
"用户可以拥有任意数量的属于同一 mint 的代币帐户,这使得其他用户很难知道他们应该将代币发送到哪个帐户。"
这意味着用户的一个代币余额可以分散在多个帐户中,而不是位于一个地方。
例如:
这就是 SPL 文档所说的“任意数量的代币帐户”的挑战。余额不是冗余副本,而是 分散在多个帐户中的总余额的一部分。
为了解决这个问题,Solana 引入了关联 Token 帐户 (ATA)。
与常规 Token 帐户(用户可以为每个 mint 拥有多个帐户)不同,ATA 是特殊的 Token 帐户(具有用于查找地址的确定性规则),它强制用户钱包地址和 mint 之间存在一对一的关系。这确保:
本文主要关注 ATA,因为由于上述常规 Token 地址的挑战,它们已成为 Solana 中管理代币的标准方法。
ATA 地址是 程序派生地址 (PDA),它是从两个输入确定性地派生出来的:
我们可以将关联 Token 帐户与以太坊的 ERC20 mapping(address => uint256) public balanceOf 进行比较,因为它们都用作跟踪用户拥有多少代币的方式。
因为用户可以有多个 SPL 代币,所以仅使用用户的地址作为密钥不足以区分不同代币的余额。这就是为什么在推导中也包含 mint 地址的原因。通过结合用户的钱包地址和代币的 mint 地址,Solana 确保每个(用户,代币)对都获得一个 唯一 的 ATA 地址。
此设计避免了冲突并强制执行一致的结构:
user_wallet_address + token_mint_address => associated_token_account_address
为了使这一点更清楚,下表比较了以太坊和 Solana 如何管理代币余额。
| 方面 | 以太坊 (ERC-20) | Solana (ATA) | 
|---|---|---|
| 存储模型 | 一个中央合约在映射中存储余额 | 每个用户为每个代币都有一个单独的帐户 (ATA) | 
| 余额位置 | 存储在代币合约中 | 存储在用户的 ATA 中 | 
| 谁为存储付费 | 合约所有者(部署成本) | 用户为其帐户付费 | 
| 查找 | 调用 balanceOf(user) | 
派生 ATA 地址 → 读取余额 | 
| 并行访问 | 受合约限制 | 完全并行 | 
两者都实现了相同的目标——跟踪代币所有权——但 Solana 的方法可以实现并行处理,因为每个余额都在一个单独的帐户中。
下图显示了关联 Token 帐户的 字段。

ATA 保存用户特定代币/mint 余额的详细信息。其关键字段包括:
mint:此帐户持有的代币(Mint 帐户)的地址。例如,如果这表示 USDC 余额,那么这将是 USDC mint 帐户的地址owner:虽然这被标记为“owner”,但它是 ATA 的权限。每个 ATA 的真正所有者始终是代币程序,因为它强制执行所有规则。此处的 owner 字段告诉代币程序必须由哪个权限签名才能进行更新或转帐。请记住 Owner vs Authority 文章,帐户的所有者强制执行其规则,而权限是唯一可以发送指令来修改帐户的有效签名者,除非此权限已通过代币程序委派了签名权。amount:此余额中持有的代币数量。delegate:已批准转帐代币的委托帐户的地址。对于一个代币帐户,一次只能存在一个委托,因为只有一个 delegate 字段。这与 ERC-20 不同,在 ERC-20 中,所有者可以批准多个消费方。state:代币帐户的状态,例如,这是一个 枚举,可以是 Uninitialized、Initialized 或 Frozen。close_authority:允许关闭帐户的地址,默认为与 owner 相同的公钥,但 owner 可以指定另一个 close_authority。当代币帐户的余额达到零时,所有者可以关闭它以收回用于租金的 SOL。诸如 Solflare 钱包 和 Sol-Incinerator 等多种 Web 工具提供了关闭空代币帐户的便捷方式。下图显示了 Token 程序、mint 帐户和 token 帐户之间的关系。

(从现在开始,当我们说“token 帐户”时,它也包括 ATA,因为 ATA 仅仅是一种特殊的 token 帐户。)
关联 Token 帐户程序具有固定的地址 ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL。它是一个链上程序,用于查找或创建给定用户代币对的正确 ATA。它处理确定性地址推导,并在需要时使用 跨程序调用 (CPI) 通过 Token 程序创建新的 ATA。
具体来说,由 ATA 程序编排的创建流程是:

与以太坊中余额隐式存在于合约存储中不同,Solana 需要显式的帐户创建。这带来了一个根本的 UX 挑战:代币不能发送给尚未显式创建接收关联 Token 帐户的用户,因为代币余额实际上存储在关联 Token 帐户中。
因此,在发送任何代币之前,我们必须为用户代币对创建 ATA。ATA 地址是使用钱包地址和 mint 地址以确定性的方式离线推导出来的。派生后,我们使用关联 Token 帐户程序在链上创建 ATA(如果它尚不存在)。这提出了一个重要的安全问题:如果任何人都可以为其他人创建 ATA,他们也可以将自己分配为 ATA 所有者和关闭权限吗?幸运的是,不能。为另一个钱包创建 ATA 时,ATA 程序强制执行 owner 和 close_authority 字段始终设置为为其创建 ATA 的钱包地址,而不是交易签名者。这种安全保证内置于 ATA 程序的代码 中,确保只有合法的钱包所有者才能保持对其代币的控制以及关闭其帐户的能力。
为了说明这一点:当 Alice 想要向 Bob 发送代币时,她会派生 Bob 的 ATA 地址,如果该地址不存在,则在链上创建它,然后使用 Bob 的 ATA 作为目标调用 Token 程序的 Transfer 指令。(在实践中,诸如 @solana/spl-token 等客户端库提供了组合 ATA 派生和创建步骤的辅助函数)。为了说明这一点:当 Alice 想要向 Bob 发送代币时,她会派生 Bob 的 ATA 地址,如果该地址不存在,则在链上创建它,然后使用 Bob 的 ATA 作为目标调用 Token 程序的 Transfer 指令。(在实践中,诸如 @solana/spl-token 等客户端库提供了组合 ATA 派生和创建步骤的辅助函数)。
下图显示了 ATA 程序的内容。

我们已经讨论了创建和管理 SPL 代币所涉及的帐户。接下来,我们将介绍 Token 程序和 ATA 程序的指令。这些指令允许你执行以下操作:创建和铸造新代币、在 ATA 之间发送代币、设置批准让其他人使用你的代币、销毁代币以减少供应量,以及关闭空帐户以收回租金。
让我们探索 Token 程序提供的公共函数,这些函数允许你与 SPL 代币交互。
请注意,在下面的指令参数中,当我们提到 token 帐户时,这些可以是常规 token 帐户或关联 Token 帐户 (ATA),如前面 Token 帐户和关联 Token 帐户部分所述。当区分很重要时,我们将明确指定我们指的是常规 token 帐户还是 ATA。
Token 程序具有以下公共函数:
InitializeMint:此指令创建一个新的 mint 帐户,该帐户表示链上的新 SPL 代币。
pub fn initialize_mint(
    mint_pubkey: &Pubkey,     // 要初始化的 mint 帐户
    decimals: u8,             // 代币的小数位数
    mint_authority: &Pubkey,  // 有权创建新代币的帐户
    freeze_authority: Option<&Pubkey> // 可选:可以冻结 token 帐户的帐户
) -> Instruction
mint_pubkey 可以是未使用的 密钥对帐户或 PDA 的地址,该地址旨在初始化为 mint 帐户。我们将在下一个教程中看到这个过程的实践。
InitializeAccount:此指令初始化一个新的常规 token 帐户(非 ATA),以保存用户特定 SPL 代币 mint 的余额。
pub fn initialize_account(
    account_pubkey: &Pubkey,   // 要初始化的 token 帐户
    mint_pubkey: &Pubkey,      // 新 token 帐户的 mint
    owner_pubkey: &Pubkey      // 新 token 帐户的所有者
) -> Instruction
ATA 由 ATA 程序初始化,该程序在底层对这个 InitializeAccount 指令执行 CPI。稍后我们将看到如何操作。
Transfer:用于将 SPL 代币单位从一个用户的 token 帐户(来源)转移到另一个用户的 token 帐户(目标)。“余额”仅仅是存储在关联 token 帐户中的一个数字,只有 SPL 程序可以修改。请注意,mint 帐户和 token 帐户必须存在,否则必须在调用 MintTo 指令之前创建(也适用于下面的 MintTo\\ 指令)。我们将在下一个教程中使用 Anchor 演示这一点。
pub fn transfer(
    source_pubkey: &Pubkey,      // 发送代币的 token 帐户(通常是发送方的 ATA;而不是 mint 帐户)
    destination_pubkey: &Pubkey, // 目标 token 帐户(代币接收到的帐户)
    authority_pubkey: &Pubkey,   // 有权从发送 token 帐户中支出的所有者或委托
    amount: u64                  // 要转移的代币数量
) -> Instruction
MintTo:此指令创建新的 代币单位 并将它们添加到指定的 token 帐户。
pub fn mint_to(
    mint_pubkey: &Pubkey,        // 代币 mint 地址
    account_pubkey: &Pubkey,     // 要铸造到的 token 帐户
    authority_pubkey: &Pubkey,   // mint 的铸造权限
    amount: u64                  // 要铸造的数量
) -> Instruction
Burn:此指令从 token 帐户中销毁指定数量的 SPL 代币单位,从而减少总代币供应量。这与 ERC20 的 burn 函数类似。
pub fn burn(
    account_pubkey: &Pubkey,     // 要从中销毁的 token 帐户
    mint_pubkey: &Pubkey,        // 代币 mint
    authority_pubkey: &Pubkey,   // token 帐户的所有者/委托
    amount: u64                  // 要销毁的数量
) -> Instruction
Approve:此指令将支出权从 token 帐户所有者委托给指定的委托,以获取最大数量。它在 token 帐户上设置 delegate 和批准的金额;一次只能存在一个委托。在该限制范围内,委托可以代表所有者转移代币。
与在合约映射中存储津贴的 ERC‑20 不同,SPL 直接在所有者的 token 帐户(通常是 ATA)上记录批准。此设计允许在单个交易中完成批准和转移,因为仅修改 token 帐户的状态。
pub fn approve(
    source_pubkey: &Pubkey,      // 授予批准的 token 帐户
    delegate_pubkey: &Pubkey,    // 委托帐户
    owner_pubkey: &Pubkey,       // 授予批准的 token 帐户的所有者
    amount: u64                  // 委托可以转移的最大代币数量
) -> Instruction
Revoke:此指令取消先前授予的任何委托批准(使用 Approve 指令进行)。它通过将 token 帐户的 delegate 字段设置为 None(无委托)来完全删除委托。
由于批准不能部分减少,因此如果要减少津贴,则必须设置具有较小金额的新批准,类似于 ERC20 减少津贴的方式。
pub fn revoke(
    source_pubkey: &Pubkey,      // 撤销批准的 token 帐户(与先前授予批准的帐户相同)
    owner_pubkey: &Pubkey        // 撤销批准的 token 帐户的所有者
) -> Instruction
FreezeAccount:此指令用于冻结 token 帐户,暂时阻止涉及该帐户中持有的代币的任何转帐或交易,直到将其解冻。换句话说,SPL 支持将用户的 token 帐户地址列入黑名单。
pub fn freeze_account(
    account_pubkey: &Pubkey,     // 要冻结的 token 帐户
    mint_pubkey: &Pubkey,        // 代币 mint
    authority_pubkey: &Pubkey    // mint 的冻结权限
) -> Instruction
ThawAccount:此指令解冻先前已冻结的 token 帐户,允许恢复代币转账和交易。
pub fn thaw_account(
    account_pubkey: &Pubkey,     // 要解冻的 token 帐户
    mint_pubkey: &Pubkey,        // 代币 mint
    authority_pubkey: &Pubkey    // mint 的冻结权限
) -> Instruction
SetAuthority:此指令更改谁持有 mint 和 token 帐户上的某些权限角色。
回想一下,mint 帐户有两个具有“权限”的字段:
mint_authorityfreeze_authority(关联的)token 帐户有两个具有“权限”的字段
owner 代币的所有者,而不是 PDA 的“Solana 运行时所有者”(此命名令人困惑)。delegate 一个可以代表所有者使用代币的公钥下面的 set_authority 中的 account_pubkey 可以指 mint 帐户或 token 帐户。
指定的 authority_type 必须与该帐户持有的权限类型匹配。
Solana SPL 的源代码 为四种权限中的每一种提供了一个枚举名称:
MintTokensFreezeAccountAccountOwnerCloseAccount请注意,SPL 程序在 token 帐户中没有以一致的方式引用权限角色,并且令人困惑地将 token 所有者称为“owner”——这不应与 PDA 的所有者混淆。
pub fn set_authority(
    account_pubkey: &Pubkey,          // mint 或 token 帐户
    current_authority_pubkey: &Pubkey, // 当前权限
    authority_type: AuthorityType,    // 要更改的权限类型(例如,MintTokens、FreezeAccount)
    new_authority_pubkey: Option<&Pubkey> // 新权限,或 None 以禁用
) -> Instruction
Revoke 具有与 SetAuthority 相同的效果,因为两者都会更改谁持有权限。Revoke 清除 token 帐户的委托(将 delegate 设置为 None),而 SetAuthority 更改 mint/帐户权限(MintTokens、FreezeAccount、AccountOwner、CloseAccount)。
CloseAccount:此指令永久关闭关联的 token 帐户,并收回用于使该帐户免租金的 SOL lamport 余额。但是,ATA 必须具有完全为零的基础 mint 代币余额,否则将返回错误。
pub fn close_account(
    account_pubkey: &Pubkey,        // 要关闭的帐户
    destination_pubkey: &Pubkey,    // 接收收回的 SOL 的帐户
    owner_pubkey: &Pubkey           // 关闭帐户的所有者
) -> Instruction
现在让我们讨论 ATA 程序的关键指令。
ATA 程序与 Token 程序一起使用,并具有以下主要指令:
Create:此指令在从钱包地址和代币 mint 地址的组合派生的确定性 PDA 地址处创建一个 ATA。如果帐户已存在于派生的地址,则该指令将失败。
pub fn create_associated_token_account(
    payer: &Pubkey,          // 为创建提供资金的帐户
    wallet_address: &Pubkey, // ATA 的钱包地址
    token_mint: &Pubkey      // 代币 mint
) -> Instruction
CreateIdempotent:确保在派生的 PDA 地址处存在正确的 ATA。如果需要,创建帐户。但是,与 Create 指令不同,即使正确的帐户已经存在,它也会成功而不会出现错误。
pub fn create_associated_token_account_idempotent(
    payer: &Pubkey,          // 为创建提供资金的帐户
    wallet_address: &Pubkey, // ATA 的钱包地址
    token_mint: &Pubkey      // 代币 mint
) -> Instruction
Create 和 CreateIdempotent 都派生 ATA 地址,然后对 Token 程序的 InitializeAccount 指令(我们在前面看到的)执行 CPI,以设置关联的 token 帐户。
总而言之,Solana 的 SPL 代币架构建立在程序逻辑与代币数据之间的根本分离之上。与在以太坊上为每个代币部署新合约不同,Solana 上的所有代币都由相同的核心 Token 程序 管理。
以下是要记住的最重要的几点:
SPL 架构的一些优势
本文是 Solana 上的教程系列 的一部分。
- 原文链接: rareskills.io/post/spl-t...
 - 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!