讨论 区块链 SPL 代币的工作原理

SPL 代币的工作原理

Joe 发表于    阅读:44    回复:0

Solana 程序库代币(SPL Token)是 Solana 上的代币标准:它规定了如何创建代币,以及代币应如何行为。SPL Token 相当于以太坊上的代币标准,例如 ERC-20(同质化代币)和 ERC-721(NFT)。

与以太坊不同——以太坊为每种代币标准使用独立的智能合约——所有 SPL 代币在 Solana 上都使用同一个程序。这意味着 Solana 上的所有代币共享相同的底层逻辑,而代币特有的参数(如名称、精度、供应量等)是在创建时设定的,而不是通过不同的程序代码实现的。SPL 代币程序只包含逻辑,所有代币数据则单独存储。这与 Solana 将逻辑与状态分离到不同账户的设计理念一致。

可以这样理解 SPL 代币与以太坊代币的区别:
在以太坊上,你通常需要为每个新代币部署一个全新的智能合约(比如一个 ERC-20 合约);而在 Solana 上,你无需部署新代码,而是与这个单一的 SPL 程序交互,该程序已经包含了定义代币、铸造、转账、授权和销毁等所有所需指令。

在以太坊上,每个代币都是一个带有自定义代码的独立智能合约,这意味着 USDC 处理授权的方式可能与 DAI 不同。这种设计在灵活性上有优势,但也可能导致意外行为。而在 Solana 上,所有 SPL 代币使用相同的转账函数、相同的授权机制和相同的安全检查

本文将解释 SPL 代币的核心概念,以及 Solana 如何将代币逻辑与代币数据分离。内容包括:

  • Solana 的代币架构与以太坊有何不同;

  • 支撑 SPL 代币运作的三个关键账户;

  • 为何 Solana 对所有代币使用同一个程序;

  • Solana 如何追踪用户的代币余额。

在下一篇文章《使用 Anchor 创建 SPL 代币》中,我们将演示如何创建和转账 SPL 代币。

要理解这些机制在实践中如何运作,我们首先从 SPL 代币程序本身入手。有时我们会简称为“代币程序”(token program),两者指的是同一个东西。

代币程序(Token Program)

SPL 代币程序是负责管理 SPL 代币功能的核心链上程序。它包含创建 SPL 代币的逻辑,并定义了代币的行为规则。该程序部署在一个固定的地址上:
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

该代币程序拥有所有存储 SPL 代币状态的账户(我们将在后文逐一介绍这些账户)。这种“所有权”意味着只有代币程序有权修改这些账户中的数据。

如果你需要回顾 Solana 的账户所有权模型,请参阅《理解 Solana 中的账户所有权》。

接下来,我们将介绍与 SPL 代币相关的几种账户类型:Mint 账户(铸币账户)、Token 账户(代币账户)以及关联代币账户(Associated Token Account, ATA)。每种账户在代币记账和转账过程中都扮演着特定角色。

Mint 账户(铸币账户)

每个独立的 SPL 代币都有一个唯一的 Mint 账户,用于存储该代币的全局信息。它保存的数据包括:

  • 代币的总供应量(total supply);

  • 小数位数(number of decimals);

  • 具有铸币权限(mint authority)的地址(如有);

  • 具有冻结账户权限(freeze authority)的地址(即“黑名单”功能)。

如前所述,代币的核心逻辑仍由 SPL 代币程序提供,而非存储在 Mint 账户中。

每个 Mint 账户都是唯一的,并在初始化一个新的 SPL 代币时通过 SPL 代币程序创建。在 Solana 中,当我们提到某个“代币地址”时,实际上指的就是它的 Mint 账户地址,因为二者是同一回事。

例如,以下是 USDC 和 USDT 的代币地址(即它们的 Mint 账户地址):EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v and Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB.




下图显示了代币计划与铸币账户之间的关系。


Mint 账户详情

和所有 Solana 账户一样,Mint 账户也包含标准的元数据字段,包括:

  • Lamports(用于支付租金)

  • Owner(在此情况下为代币程序的地址)

  • Executable 状态(值为 false,因为该账户仅用于存储数据)

如需了解更多关于 Solana 账户的内容,请参阅《在 Solana 和 Anchor 中初始化账户》。

除了这些标准字段外,Mint 账户还包含定义代币本身的特定数据字段:

  • mint_authority:被授权铸造新代币的地址。

  • freeze_authority:被授权冻结持有该代币的账户的地址(即“黑名单”功能)。

  • decimals:代币使用的小数位数(0–9)。

  • supply:已创建的代币总量。

  • is_initialized:布尔标志,防止重复初始化。

需要注意的是,Mint 账户仅存储代币的全局信息,并不记录各个用户的余额——用户余额由我们稍后将介绍的其他账户来管理。

下图展示了 Mint 账户的属性。

如上图所示,Mint 账户的 mint_authority 和 freeze_authority 在创建时分配,通常设为交易签名者的地址。我们将在“代币程序指令”部分看到相关的创建指令。

Mint 账户的一个重要特性是它如何控制代币的供应量,这通过 mint_authority 字段实现。下面我们将详细说明。

如何设置代币的最大供应量

SPL 代币通过移除权限而非显式设置上限的方式来实现最大供应量。这一设计源于 Solana 与以太坊在状态管理上的根本差异。

Mint 账户的数据结构中没有“max supply”(最大供应量)字段。因此,若要创建固定供应量的代币,你需要将 mint_authority 设置为 None(空值),从而永久禁用铸币权限。一旦 mint_authority 被设为 None,就没有任何账户可以再铸造新代币,当前的总供应量也就成为最终的固定上限。

相比之下,在以太坊上,限制代币总供应量的传统做法是显式存储一个最大值,并在尝试超发时阻止铸造操作。而 SPL 代币没有“总供应上限”的概念,也不会在任何地方存储这样的数值。

在 SPL 中,如果你希望总供应量为 100 万枚代币,你只需先铸造全部 100 万枚并分发给持有者,然后将 mint_authority 设为 None。或者,正如我们将在后续教程中看到的那样,你也可以指定另一个程序作为 mint_authority,并让该程序在达到供应上限后停止铸造。

代币账户与关联代币账户(ATAs)

如前所述,Mint 账户只存储代币本身的元信息。为了追踪每个用户的余额,Solana 使用一种称为 Token Account(代币账户)的独立账户。

代币账户(Token Accounts)

代币账户是 Solana 账户的一种,用于存储:

  • 用户持有的某一代币的余额;

  • 该账户所关联的 Mint 地址;

  • 可授权转账的账户所有者(owner);

  • 以及其他我们稍后会详细介绍的字段。

根据设计,一个用户可以为同一种代币拥有多个代币账户,这些账户位于不同的地址上。这带来了 Solana 程序库文档中指出的一个挑战:

“一个用户可能拥有任意多个属于同一 Mint 的代币账户,这使得其他用户难以知道应该向哪个账户发送代币。”

这意味着,用户的某一代币余额可能分散在多个账户中,而不是集中在一个地方

例如:

  • Alice 在一个代币账户中有 5 枚代币,在另一个账户中有 15 枚;

  • 这两个账户都属于同一个 Mint,因此她总共拥有 20 枚该代币;

  • 但如果 Bob 想给 Alice 转账,他无法轻易知道 Alice 希望接收代币的具体账户是哪一个。

这就是 SPL 文档所说的“任意多个代币账户”带来的问题——这些余额不是冗余副本,而是总余额被拆分到多个账户中的碎片

为了解决这个问题,Solana 引入了 关联代币账户(Associated Token Account, ATA)。

关联代币账户(Associated Token Accounts, ATAs)

与普通代币账户不同,ATA 是一种特殊的代币账户,其地址通过确定性规则生成,并强制在用户钱包地址与代币 Mint 之间建立一对一关系。这确保了:

  • 每个用户对每种代币有且仅有一个可预测的 ATA

  • 任何应用程序都可以在无需预先配置的情况下,轻松找到用户的代币余额,因为地址是确定性生成的(下文将解释原理)。

由于上述普通代币账户带来的用户体验问题,ATA 已成为 Solana 上管理代币的标准方式,本文也将主要聚焦于 ATA。

ATA 地址是如何生成的?

ATA 地址是一种 程序派生地址(Program Derived Address, PDA),由以下两个输入确定性地推导而来:

  1. 用户/签名者钱包的地址(即预期的 authority);

  2. 代币的 Mint 账户地址。

我们可以将 ATA 类比为以太坊 ERC-20 合约中的 mapping(address => uint256) public balanceOf,因为两者都用于追踪用户拥有的代币数量。

但由于一个用户可能持有多种 SPL 代币,仅用用户地址作为键不足以区分不同代币的余额。因此,Mint 地址也被纳入地址推导过程。通过组合用户钱包地址和代币 Mint 地址,Solana 确保每个 (用户, 代币) 组合都对应一个唯一的 ATA 地址。

这种设计避免了冲突,并建立了统一的结构:

user_wallet_address + token_mint_address ⇒ associated_token_account_address

为更清晰起见,下表对比了以太坊和 Solana 管理代币余额的方式:

AspectEthereum (ERC-20)Solana (ATAs)
Storage ModelOne central contract stores balances in a mappingEach user has a separate account (ATA) for each token
Balance LocationStored inside the token contractStored inside the user’s ATA
Who Pays for StorageThe contract owner (deployment cost)

The user pay for their account

LookupCall balanceOf(user)

Derive ATA address → read balance

Parallel AccessLimited by contract

Fully parallel

两者目标一致——追踪代币所有权——但 Solana 的设计支持并行处理,因为每个余额都位于独立的账户中。

下图展示了关联代币账户的字段结构。

ATA 存储了用户对某一特定代币(Mint)的余额详情,其关键字段包括:

  • mint:该账户所代表的代币的 Mint 账户地址。例如,如果是 USDC 余额,则此处为 USDC 的 Mint 地址。

  • owner:虽然名为“owner”,但实际上是该 ATA 的授权方(authority)。请注意,每个 ATA 的真正所有者(Owner)始终是代币程序,因为它执行所有规则。这里的 owner 字段告诉代币程序:谁必须签名才能执行转账或更新操作

    回顾《Owner 与 Authority 的区别》一文:账户的 Owner 负责执行规则,而 Authority 是唯一有权发送指令修改账户的签名者(除非 Authority 通过代币程序委托了签名权)。

  • amount:该账户中持有的代币数量。

  • delegate:被授权代表用户转账的委托地址。每个代币账户只能有一个 delegate(因为只有一个 delegate 字段),这与 ERC-20 不同(ERC-20 允许 owner 授权多个 spender)。

  • state:账户状态,是一个枚举值,可为 Uninitialized(未初始化)、Initialized(已初始化)或 Frozen(已冻结)。

  • close_authority:被授权关闭该账户的地址。默认与 owner 相同,但 owner 可指定其他地址。当账户余额归零时,owner 可关闭账户以收回用于支付租金的 SOL。许多工具(如 Solflare 钱包、Sol-Incinerator)提供了便捷的空账户清理功能。

下图展示了代币程序、Mint 账户与代币账户之间的关系。

(从此以后,当我们提到“代币账户”时,也包括 ATA,因为 ATA 只是代币账户的一种特殊类型。)

关联代币账户程序(Associated Token Account Program)

关联代币账户程序的固定地址为:
ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL

这是一个链上程序,用于为给定的用户-代币对查找或创建正确的 ATA。它负责确定性地址推导,并在需要时通过跨程序调用(CPI)调用代币程序来创建新的 ATA。

具体来说,ATA 程序协调的创建流程如下:

与以太坊不同——在以太坊中,余额隐式存在于合约存储中——Solana 要求显式创建账户。这带来了一个根本性的用户体验挑战:你无法向尚未创建接收账户的用户发送代币,因为代币余额实际上就存储在 ATA 中。

因此,在发送任何代币之前,我们必须为该用户-代币对创建 ATA

  • 首先,离线(off-chain)使用用户钱包地址和 Mint 地址确定性地推导出 ATA 地址;

  • 然后,如果该 ATA 尚未存在,就通过关联代币账户程序在链上创建它。

这引发了一个重要的安全问题:如果任何人都能为他人创建 ATA,他们是否也能将自己设为该 ATA 的 owner 或 close_authority?

幸运的是,不会
当为另一个钱包创建 ATA 时,ATA 程序强制规定

  • ownerclose_authority 字段必须设为该 ATA 所属的钱包地址

  • 而不是交易签名者的地址

这一安全保证内置于 ATA 程序的代码中,确保只有合法的钱包所有者才能控制其代币和关闭账户的权利。

举例说明
当 Alice 想给 Bob 发送代币时:

  1. 她先推导出 Bob 的 ATA 地址;

  2. 如果该 ATA 不存在,就在链上创建它;

  3. 然后调用代币程序的 Transfer 指令,将 Bob 的 ATA 作为目标地址。

实际开发中,像 @solana/spl-token 这样的客户端库提供了辅助函数,可自动完成 ATA 推导与创建步骤。

下图展示了 ATA 程序的内容。

总结

我们已经讨论了创建和管理 SPL 代币所涉及的各类账户。接下来,我们将介绍代币程序ATA 程序的指令。这些指令使你能够:

  • 创建并铸造新代币;

  • 在 ATA 之间转账;

  • 授权他人代为支出你的代币;

  • 销毁代币以减少供应量;

  • 关闭空账户以收回租金。

这些功能共同构成了 Solana 上代币生态的基石。

代币程序说明

让我们来探索代币计划提供的公共功能,这些功能允许您与 SPL 代币进行交互。

请注意,在以下指令参数中,当我们提及令牌账户时,它们既可以是普通令牌账户,也可以是关联令牌账户 (ATA),如前文“令牌账户和关联令牌账户”部分所述。当需要区分时,我们会明确指出指的是普通令牌账户还是关联令牌账户。

代币程序具有以下公共功能:

InitializeMint:此指令创建一个新的铸币账户,该账户代表链上的新 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 可以是未使用的密钥对账户的地址 ,也可以是 计划初始化为铸币账户的PDA的地址。我们将在下一篇教程中实际演示这个过程。

InitializeAccount:此指令初始化一个新的常规代币账户(非ATA),用于保存用户在特定SPL代币铸造期间的余额。

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


ATA 由 ATA 程序初始化,该程序在InitializeAccount底层执行 CPI 指令。我们稍后会看到具体细节。

Transfer此指令用于将 SPL 代币从一个用户的代币账户(源账户)转移到另一个用户的代币账户(目标账户)。“余额”是指存储在关联代币账户中的一个数字,只有 SPL 程序才能修改该数字。请注意,在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此指令创建新的代币单位,并将其添加到指定的代币帐户。

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此指令会从代币账户中销毁指定数量的 SPL 代币,从而减少代币总供应量。其工作原理类似于 ERC20 的销毁功能。

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此指令将代币账户所有者的支出权限委托给指定的代理人,并设定最大金额限制。它会设置代币账户的授权码 delegate 和账户的授权金额;同一时间只能存在一个代理人。在授权金额限制内,代理人可以代表所有者转移代币。

与将授权值存储在合约映射中的 ERC-20 不同,SPL 直接将批准记录在所有者的代币账户(通常是 ATA)中。这种设计使得批准和转账可以在单个交易中完成,因为仅修改了代币账户的状态。

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从而彻底移除委托 。delegateNone

由于审批额度不能部分减少,如果您想降低限额,则必须设置一个金额较小的新审批额度,类似于 ERC20 减少限额的方式。

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此指令用于冻结代币账户,暂时阻止任何涉及该账户中代币的转账或交易,直至账户解冻。换句话说,SPL 支持将用户的代币账户地址列入黑名单。

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此指令可解冻先前冻结的代币账户,从而恢复代币转移和交易。

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_authority

  • freeze_authority

关联的令牌账户有两个字段名为“authority”。

  • owner是代币的所有者,而不是 PDA 的“Solana 运行时所有者”(这种命名容易引起混淆)。

  • delegate一个可以代表所有者花费代币的公钥

下面的“ account_pubkeyin”set_authority既可以指铸币账户,也可以指代币账户。

指定的权限authority_type必须与该账户所拥有的权限类型相符。

Solana的 SPL 源代码为四种授权类型中的每一种都给出了一个枚举名称:

  • MintTokens

  • FreezeAccount

  • AccountOwner

  • CloseAccount

请注意,SPL 程序在代币账户中没有以一致的方式提及权限角色,并且令人困惑地将代币所有者称为“所有者”——这不应与 PDA 的所有者混淆。

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 两者都会改变谁拥有权限。 Revoke 清除代币账户的委托(设置 delegate 为 None),而 SetAuthority 更改铸币/账户权限(MintTokens,  FreezeAccount,  AccountOwner)  CloseAccount

CloseAccount此指令将永久关闭关联的代币账户,并收回用于使该账户免租的 SOL 导入余额。但是,ATA 中基础铸造代币的余额必须正好为零,否则将返回错误。

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程序的关键指令。

关联令牌账户 (ATA) 程序说明

ATA程序与Token程序配合使用,其主要指令如下:

Create此指令会在由钱包地址和代币铸造地址组合而成的确定性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 地址,然后执行 CPI 到令牌程序的 InitializeAccount指令(我们之前已经看到过),以设置关联的令牌帐户。

概括

总而言之,Solana 的 SPL 代币架构基于程序逻辑与代币数据的基本分离。与以太坊为每个代币部署新合约不同,Solana 上的所有代币都由同一个核心代币程序管理。

以下是需要记住的最重要几点:

  • 逻辑与状态:单一代币程序包含所有规则(转账、铸造、销毁),但本身不保存任何代币数据(余额、小数位数、供应量)。它作为所有 SPL 代币的通用逻辑引擎。

  • 铸币账户即代币:铸币账户定义了一个唯一的代币。它存储代币的全局信息,例如总供应量、小数位数以及拥有增发权限的机构。铸币账户的地址即为代币的地址(例如,USDC 的铸币地址)。

  • 余额存储在代币账户/关联代币账户 (ATA) 中: 用户余额存储在独立的代币账户或关联代币账户 (ATA) 中。与使用一个大型合约将地址映射到余额不同,每个用户都会为其拥有的每种代币类型创建一个单独的账户。ATA 的地址由用户的钱包地址和代币的铸造地址共同生成,这是推荐的解决方案。

SPL架构的一些优势

  • 并行处理:由于每个用户的余额都在单独的账户中,网络可以同时处理数千笔转账,而不会互相干扰。

  • 标准化:所有 SPL 代币,无论是稳定币还是概念币,都遵循核心代币计划的完全相同的安全逻辑。这降低了自定义代币合约可能出现的漏洞风险。




原文:https://rareskills.io/post/spl-token

免责声明:本文为c2e Labs的第三方内容,仅供信息分享与传播之目的,不代表我们的立场或观点且不构成任何投资及应用建议。版权归原作者或来源方所有,如内容或素材有所争议请和我们取得联系。

我来评论