说到代币标准,ERC-20 和 SPL-Token 无疑是区块链世界两大主流生态—以太坊与 Solana—中最核心的代表。不过,它们背后的设计理念却大相径庭,不仅体现了技术实现上的分歧,更是设计哲学上的取舍。
从软件架构的视角看,ERC-20 更像一个“中央账本”,采用高度集中的包含模式;而 SPL-Token 则倾向于“分布式账本”,通过组合模式将状态分散管理。这种根本差异直接影响了两者在性能、扩展性和数据查询上的表现。
下面我们将从设计模式、状态查询机制和底层执行模型三个层面,系统剖析它们的区别与背后的权衡。
一、设计模式:中心化与分散化的根本分歧
在软件架构中,设计选择往往决定了系统的扩展上限和并发能力。ERC-20 和 SPL-Token 在这一点上走向了两个方向。
ERC-20:中央总账式设计
// ERC20Bank.java - 「包含模式」或「中央账本模式」
import java.util.HashMap;
import java.util.Map;
public class ERC20Bank {
// 核心:一个内置的、共享的映射,维护所有状态
private Map<String, Integer> balances = new HashMap<>();
private String bankName;
public ERC20Bank(String name) {
this.bankName = name;
}
// 所有操作都必须通过这个中心化的类实例的方法进行
public synchronized void transfer(String from, String to, int amount) {
// 1. 检查发送者余额
int fromBalance = balances.getOrDefault(from, 0);
if (fromBalance < amount) {
throw new RuntimeException("Insufficient funds");
}
// 2. 更新中央账本中的两个条目
balances.put(from, fromBalance - amount);
balances.put(to, balances.getOrDefault(to, 0) + amount);
System.out.println("Transferred " + amount + " from " + from + " to " + to + " in bank " + bankName);
}
public synchronized int getBalance(String accountId) {
return balances.getOrDefault(accountId, 0);
}
public synchronized void setBalance(String accountId, int amount) {
balances.put(accountId, amount);
}
}
使用方式与问题分析
public class Main {
public static void main(String[] args) {
ERC20Bank bank = new ERC20Bank("Central Bank");
// 初始化账户
bank.setBalance("Alice", 1000);
bank.setBalance("Bob", 500);
// 执行转账
bank.transfer("Alice", "Bob", 200); // Alice -> Bob
// 问题:并发瓶颈
// 两个无关的转账(Alice->Bob 和 Charlie->David)无法同时进行。
// 因为 `transfer` 方法是 `synchronized` 的,所有操作都在争夺“银行实例”这把锁。
// 即使操作的是完全不同的账户,也必须串行排队。
System.out.println("Alice's balance: " + bank.getBalance("Alice")); // 800
System.out.println("Bob's balance: " + bank.getBalance("Bob")); // 700
}
}
关键点:
-
• 状态内置:所有账户数据( balances)都封装在ERC20Bank对象内部。 -
• 强耦合:客户(Alice, Bob)没有独立的存在,他们只是中央账本里的一个键名。 -
• 全局锁:任何操作(即使修改不同账户)都需要获取整个银行对象的锁( synchronized),导致并发能力差。
SPL-Token:独立账户式设计
这就像银行系统不管理具体余额,而是为每个客户开设一个独立的保险箱(Vault)。银行(Mint)只定义规则,而钱存放在各个保险箱里。
首先,我们定义“代币”规则(Mint Account):
// TokenMint.java - 代币规则
public class TokenMint {
private String name;
private String symbol;
private int decimals;
public TokenMint(String name, String symbol, int decimals) {
this.name = name;
this.symbol = symbol;
this.decimals = decimals;
}
// Mint主要定义属性,没有余额概念
public String getName() { return name; }
public String getSymbol() { return symbol; }
}
然后,定义客户的保险箱(Token Account):
// TokenAccount.java - 代币账户
public class TokenAccount {
// 组合:外置的依赖
private TokenMint mint; // 这个账户存的是哪种代币?
private String owner; // 这个保险箱属于谁?
private int balance; // 余额是多少?
public TokenAccount(TokenMint mint, String owner) {
this.mint = mint;
this.owner = owner;
this.balance = 0;
}
public void transferTo(TokenAccount to, int amount) {
// 转账逻辑只涉及当前两个账户
if (this.balance < amount) {
throw new RuntimeException("Insufficient funds in account of " + owner);
}
this.balance -= amount;
to.balance += amount;
System.out.println("Transferred " + amount + " " + mint.getSymbol() + " from " + owner + "'s account to " + to.owner + "'s account.");
}
public int getBalance() {
return balance;
}
public void mintTo(int amount) {
balance += amount;
}
}
使用方式与优势:
public class Main {
public static void main(String[] args) {
// 1. 创建一种代币规则(例如USDC)
TokenMint usdcMint = new TokenMint("USD Coin", "USDC", 6);
// 2. 为Alice和Bob创建独立的代币账户(保险箱)
TokenAccount alicesAccount = new TokenAccount(usdcMint, "Alice");
TokenAccount bobsAccount = new TokenAccount(usdcMint, "Bob");
// 3. 给Alice的账户铸一些币
alicesAccount.mintTo(1000);
// 4. 执行转账:Alice 转账给 Bob
// 注意:操作是在两个账户对象之间直接发生的!
alicesAccount.transferTo(bobsAccount, 200);
// 优势:并发潜力
// 假设还有Charlie和David的账户:
// TokenAccount charliesAccount = ...;
// TokenAccount davidsAccount = ...;
//
// 转账 Alice->Bob 和 Charlie->David 是完全独立的。
// 它们操作的是不同的对象 (alicesAccount+bobsAccount vs charliesAccount+davidsAccount)。
// 在多线程环境下,这两笔交易可以并行执行,不需要争夺同一把锁。
System.out.println("Alice's balance: " + alicesAccount.getBalance()); // 800
System.out.println("Bob's balance: " + bobsAccount.getBalance()); // 200
}
}
关键点:
-
• 状态外置:状态(余额)分散在各个独立的 TokenAccount对象中。TokenMint对象只负责定义规则,不存储状态。 -
• 松耦合: TokenAccount和TokenMint是组合关系(has-a),而非包含关系。账户是独立的一等公民。 -
• 细粒度锁:如果要在多线程中转账,只需要锁住相关的那两个 TokenAccount对象即可。不相关的转账可以并行处理,并发能力强。
特性对比
|
|
|
|
| 设计模式 |
|
|
| 状态存储 |
HashMap)。
|
TokenAccount实例)。
|
| 依赖关系 |
|
|
| 并发类比 | synchronized方法
|
synchronized块
|
| 扩展性 |
|
|
💡 设计启示:
SPL-Token 的组合模式天然适合并发,是传统软件中“避免全局锁”原则的链上实践;而 ERC-20 的集中模式虽限制了扩展性,但换来了状态的一致性简化。
二、状态查询:便利性与性能的取舍
状态查询是代币系统另一个核心场景,两者表现出明显不同的特点。
ERC-20 的“包含模式”在全局状态查询上具有天然优势,而 SPL Token 的“组合模式”为了实现并行化和扩展性,牺牲了全局数据读取的简便性。
这完美地印证了计算机科学中的一个核心原则:There are no solutions, only trade-offs. 。
ERC-20:全局查询的“简易模式”
在 ERC-20 合约中,所有状态都位于单一合约的单一存储映射中。
mapping(address => uint256) private _balances;
优势:
-
• 查询简单: 理论上,如果有一个类似 getAllHolders的函数(标准里没有,但可以实现),它可以直接遍历这个映射并返回所有数据。 -
• 索引完整: 所有持有者信息天然地、强制性地被集中索引在同一个地方。
实现全局查询的途径(即使在没有原生遍历功能的EVM中):
-
1. 链下索引: 像 The Graph、Etherscan 这样的索引器可以监听所有的 Transfer事件。由于所有转账都来自同一个合约地址,它们可以相对容易地构建和维护一个完整的持有者数据库。 -
2. 中心化数据源: 获取一个 ERC-20 代币的全貌,只需要扫描一个合约地址的历史日志即可。
SPL Token:全局查询的“困难模式”
在 SPL Token 中,状态分散在数百万甚至数千万个独立的 Token Account(尤其是 ATA) 中。
劣势:
-
• 没有中心索引: 没有一個地方天然地知道“谁创建了哪些代币账户”。单是找到所有存在的代币账户就是一个巨大挑战。 -
• 数据源分散: 持有者信息分布在无数个独立的账户里,要收集它们,就像要把撒在一片广阔草原上的所有特定颜色的弹珠都找出来。
实现全局查询的途径(非常复杂):
-
1. 遍历整个账本(不可行) : 直接扫描 Solana 浩如烟海的账户集合来寻找特定 Mint 的代币账户,在计算和经济上都是完全不可行的。 -
2. 依赖“上帝视角”的索引器: 这是唯一现实的方法。需要有一个强大的、始终在线的索引器(如 Solana FM, Solscan, Dune Analytics, Helius 等)来: -
• 监听所有交易: 抓取链上发生的每一笔与 spl-token程序相关的指令。 -
• 解析指令: 当看到 spl_token::instruction::transfer或spl_token::instruction::initialize_account这样的指令时,解析出涉及的 Mint 地址、源账户、目标账户等信息。 -
• 构建并维护数据库: 在链下的数据库里,为每一个 Mint 地址维护一个列表,记录所有持有它的 Token Account 地址及其余额。 -
• 提供API: 最终通过 API 向用户或前端提供查询服务,例如:“给我列出所有持有 MINT_X代币的前100个地址”。
编程类比:数据库 vs 文件系统
这很像传统编程中数据库和文件系统的对比:
-
• ERC-20 像数据库:
数据高度结构化,集中存储,查询优化得好。-- 查询所有用户(假设有这样一个表)
SELECT * FROM balances WHERE token_contract = '0x...'; -
• SPL Token 像文件系统:
每个用户的余额像一个独立的文本文件。
想知道USDC的总供应量?则必须遍历整个/tokens/usdc/accounts/user_A_account.txt -> Balance: 100
/tokens/usdc/accounts/user_B_account.txt -> Balance: 200
/tokens/usdc/accounts/user_C_account.txt -> Balance: 50/tokens/usdc/accounts/目录下的所有文件,把里面的数字加起来。这个操作非常重。
性能与便利性的经典权衡
|
|
|
|
| 写入性能 (转账) | 差
|
优
|
| 读取性能 (全局查询) | 优
|
差
|
| 设计哲学 |
|
|
Solana 的设计哲学非常明确:优先保证链上执行(写入)的高性能和低费用。它默认将复杂查询这种重计算、高频率的操作“卸载”到专业的链下索引基础设施中去。
所以在链上不是“不容易”获取SPL持有者信息,而是几乎无法直接获取。必须依赖那些做了繁重解析工作的第三方索引服务。这就是为了换取极致吞吐量所必须付出的架构代价
三、执行引擎与编程范式:单线程与多线程的底层差异
从上文的分析可以看出SPL-Token属于多线程编程范式而ERC20属于单线程编程范式,那么为什么在solana链上选择使用SPL-Token这种多线程编程范式而在以太坊上选择ERC20这种单线程编程范式呢。换句话说,如果我们在以太坊上采用SPL-Token这种编程范式,或者反之,在solana上使用ERC20单线程范式会不会有问题呢?
这个问题的答案就在于底层区块链执行引擎的本质化差异。
以太坊:单线程全球计算机
EVM 本质是单线程处理器:
-
• 所有交易串行执行,按顺序修改全局状态树 -
• 网络拥堵时,Gas 费本质是竞拍下一个CPU时间片的价格 -
• 不管架构如何设计,最终都逃不开全局争用
Solana:多线程并行处理器
Solana 被设计为多线程机器:
-
• 交易执行前需声明读写账户集合 -
• 无冲突的交易被调度到不同核心并行执行 -
• SPL 账户模型正好适配: A→B和C→D互不冲突,可同时处理
核心差异对比
|
|
Solana (SPL Token) | Ethereum (模拟 SPL) |
| 执行引擎 | 并行(多核)
|
串行(单核)
|
| 状态模型 | 分散式
|
集中式
|
| 架构匹配 | 匹配
|
不匹配
|
| 性能结果 | 1+1>2
|
1+1<1
|
从智能合约层面,可以在以太坊上模拟出 SPL Token 的“组合模式”,但由于以太坊底层虚拟机的执行模型是串行的,这种模拟无法带来任何性能提升,反而会大大增加复杂性和成本。
让我们来深入剖析一下为什么。
1. 如何在以太坊上模拟 SPL 的“组合模式”?
理论上,我们可以部署一套智能合约来模仿 SPL 的架构:
-
• 一个主管理器合约(模拟 Mint 的规则部分) :记录代币名称、符号、总供应量等。 -
• 为每个用户部署独立的代理合约(模拟 Token Account) :每个用户的余额存储在他们自己的代理合约中。
转账流程会变得非常复杂:
-
1. 用户 A 要转账给用户 B。 -
2. 用户 A 调用自己的代理合约 A。 -
3. 代理合约 A 调用主管理器合约进行权限和规则校验。 -
4. 主管理器合约再调用用户 B 的代理合约 B。 -
5. 最后,代理合约 B 完成余额增加。
这个过程涉及多个跨合约调用(Cross-Contract Call, CCC),在以太坊上,每一步都需要消耗昂贵的 Gas。
2. 为什么这种模拟无法提升速度?核心瓶颈:全局状态和串行执行
即使架构模仿得再像,以太坊的两个底层设计也彻底扼杀了其性能提升的可能性:
全局共享状态 & 串行执行(单线程EVM)
这是最根本的原因。我们可以把以太坊虚拟机(EVM)想象成一个单核CPU。
-
• 在 Solana 上:SPL 的分散账户模型与它的并行执行引擎(Sealevel)是天生一对。运行时可以分析出转账 A->B 和 C->D 操作的是不同的账户(不同的“数据通道”),因此可以将它们分发到不同的核心同时处理。 -
• 在以太坊上:无论我们的合约架构多么分散,所有交易最终都必须由这个“单核CPU”(EVM)按顺序执行。即便两笔交易处理的是完全不同的代理合约,它们也必须排队,一个一个地被处理。EVM 无法同时执行它们。
这就像用单核CPU运行多线程程序。虽然代码是“多线程”的,但底层硬件无法提供真正的并行计算能力,所有线程最终还是在时间片上切换,共享同一个核心的计算资源,性能不会有本质提升。
设计与底层必须协同工作
高性能系统的设计是一个整体工程,而不仅仅是应用层的巧妙架构。
-
• Solana 的高性能来自于其应用层设计(SPL Token 组合模式) 与底层执行引擎(并行处理、分散状态) 的协同优化。两者相互促进,缺一不可。 -
• 在以太坊上,其性能天花板是由其底层虚拟机(EVM)和状态模型决定的。在应用层做的任何优化,如果脱离了底层的支持,其效果都会大打折扣,甚至适得其反。
这就像试图在一条乡村小路(以太坊底层)上通过优化交通规则(合约架构)来解决拥堵问题,其效果有限。而 Solana 的做法是直接修建了一条拥有多车道、立体交通系统的高速公路(底层),并制定了与之匹配的交通规则(SPL标准)。
因此,仅仅在合约层面采用“组合模式”,而底层执行环境仍是串行的,无法为以太坊带来执行速度上的提升。 以太坊生态寻求性能提升的路径主要依赖于 Layer 2 扩容方案(如 Rollups),这些方案的本质是在链下创建一个并行的执行环境,反而在某种程度上更接近于 Solana 的哲学。
四、总结与展望
ERC-20 与 SPL-Token 的差异,深刻体现了区块链架构设计中的核心权衡:如何在一致性、性能与可扩展性之间取得平衡。当前两者的分野,为我们勾勒出下一代代币标准的演进方向——未来的架构设计将不再局限于单一链上实现,而是走向多层次、多链协同的异构架构。
未来的代币架构演进将呈现以下趋势:
1. 模块化与可组合架构
-
• 代币标准将逐渐解构为可插拔模块,开发者可按需选择一致性模块、性能模块或隐私模块 -
• 类似 Token-2022 的扩展程序将成为标配,支持自定义逻辑和业务规则 -
• 智能合约钱包将作为执行代理,实现复杂的跨链资产管理和权限控制
2. 分层架构成为主流
-
• 结算层:依托以太坊等高安全性链完成最终清算和状态锚定 -
• 执行层:在 Solana 等高性能链上处理交易和状态转换 -
• 数据可用层:采用 Celestia 等专用网络保证数据可验证性 -
• 应用层:提供统一的用户接口,屏蔽底层链差异
3. 跨链智能账户体系
-
• 智能账户将同时管理多链资产,根据交易类型自动选择最优执行链 -
• 状态同步协议保证跨链操作的一致性,实现真正的互操作体验 -
• 零知识证明确保跨链状态转换的可验证性,避免信任假设
4. 面向特定场景的优化架构
-
• DeFi 应用:采用以太坊主网+ZK-Rollup 架构,平衡安全与性能 -
• 游戏社交:基于 Solana 等高吞吐链构建,支持大规模并发用户 -
• 企业应用:依托联盟链提供合规框架,同时与公链实现资产互通
未来的代币架构将仅仅局限于单一链,而是充分利用各条链的独特优势,构建出既安全又高效的多层体系。对于开发者而言,重要的是树立架构思维——不再问"应该选择哪条链",而是思考"如何设计跨链架构",让不同的链各司其职,共同支撑应用需求。
这种架构演进最终将推动区块链进入真正的主流应用时代,让用户体验到既安全又流畅的链上服务,而背后的技术复杂性,将被完善的基础设施和开发工具所完全隐藏。

