软件架构实践之ERC-20和SPL-Token

  • 游庄
  • 发布于 2天前
  • 阅读 170

说到代币标准,ERC-20和SPL-Token无疑是区块链世界两大主流生态—以太坊与Solana—中最核心的代表。不过,它们背后的设计理念却大相径庭,不仅体现了技术实现上的分歧,更是设计哲学上的取舍。

<!--StartFragment-->

 

说到代币标准,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&lt;String, Integer> balances = new HashMap&lt;>();
    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 &lt; 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 &lt; 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对象只负责定义规则,不存储状态。
  • • 松耦合TokenAccountTokenMint是组合关系(has-a),而非包含关系。账户是独立的一等公民。
  • • 细粒度锁:如果要在多线程中转账,只需要锁住相关的那两个TokenAccount对象即可。不相关的转账可以并行处理,并发能力强

特性对比

特性 ERC-20 (包含模式) SPL Token (组合模式)
设计模式 一个中心类包含所有数据和行为。 多个对象组合在一起协作,各自管理自己的状态。
状态存储 内置的集中式映射(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. 1. 链下索引: 像 The Graph、Etherscan 这样的索引器可以监听所有的 Transfer 事件。由于所有转账都来自同一个合约地址,它们可以相对容易地构建和维护一个完整的持有者数据库。
  2. 2. 中心化数据源: 获取一个 ERC-20 代币的全貌,只需要扫描一个合约地址的历史日志即可。

SPL Token:全局查询的“困难模式”

在 SPL Token 中,状态分散在数百万甚至数千万个独立的 Token Account(尤其是 ATA)  中。\ 劣势:

  • • 没有中心索引: 没有一個地方天然地知道“谁创建了哪些代币账户”。单是找到所有存在的代币账户就是一个巨大挑战。
  • • 数据源分散: 持有者信息分布在无数个独立的账户里,要收集它们,就像要把撒在一片广阔草原上的所有特定颜色的弹珠都找出来。

实现全局查询的途径(非常复杂):

  1. 1. 遍历整个账本(不可行) : 直接扫描 Solana 浩如烟海的账户集合来寻找特定 Mint 的代币账户,在计算和经济上都是完全不可行的。
  2. 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 像文件系统:\ 每个用户的余额像一个独立的文本文件。

    /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

    想知道USDC的总供应量?你必须遍历整个 /tokens/usdc/accounts/ 目录下的所有文件,把里面的数字加起来。这个操作非常重。

性能与便利性的经典权衡

特性 ERC-20 (包含模式) SPL Token (组合模式)
写入性能 (转账) : 所有交易竞争一个合约,串行执行。 : 无关交易可并行处理,吞吐量高。
读取性能 (全局查询) : 状态集中,易于索引和查询。 : 状态分散,必须依赖外部索引器构建二次索引。
设计哲学 优化读取便利性,牺牲写入扩展性。 优化写入扩展性和并行性,牺牲读取便利性。

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)
执行引擎 并行(多核) :Sealevel 运行时 串行(单核) :EVM
状态模型 分散式:账户状态分散存储,天然支持分片 集中式:所有合约状态共享一个全局存储树
架构匹配 匹配:组合模式的设计与底层并行引擎完美契合 不匹配:组合模式的架构被强塞进串行执行的虚拟机中
性能结果 1+1>2:架构优势被底层引擎放大 1+1<1:架构优势被底层引擎扼杀,且引入额外开销

从智能合约层面,可以在以太坊上模拟出 SPL Token 的“组合模式”,但由于以太坊底层虚拟机的执行模型是串行的,这种模拟无法带来任何性能提升,反而会大大增加复杂性和成本。

让我们来深入剖析一下为什么。

1. 如何在以太坊上模拟 SPL 的“组合模式”?

理论上,我们可以部署一套智能合约来模仿 SPL 的架构:

  • • 一个主管理器合约(模拟 Mint 的规则部分) :记录代币名称、符号、总供应量等。
  • • 为每个用户部署独立的代理合约(模拟 Token Account) :每个用户的余额存储在他们自己的代理合约中。

转账流程会变得非常复杂:

  1. 1. 用户 A 要转账给用户 B。
  2. 2. 用户 A 调用自己的代理合约 A
  3. 3. 代理合约 A 调用主管理器合约进行权限和规则校验。
  4. 4. 主管理器合约再调用用户 B 的代理合约 B
  5. 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 等高吞吐链构建,支持大规模并发用户
  • • 企业应用:依托联盟链提供合规框架,同时与公链实现资产互通

5. 开发者体验革新

  • • 跨链开发框架抽象底层差异,提供统一的 API 接口
  • • 仿真测试环境支持多链合约联合调试
  • • 自动化部署工具实现一键多链部署

未来的代币架构将不再是单一链的囚徒,而是充分利用各条链的独特优势,构建出既安全又高效的多层体系。对于开发者而言,重要的是树立架构思维——不再问"应该选择哪条链",而是思考"如何设计跨链架构",让不同的链各司其职,共同支撑应用需求。

这种架构演进最终将推动区块链进入真正的主流应用时代,让用户体验到既安全又流畅的链上服务,而背后的技术复杂性,将被完善的基础设施和开发工具所完全隐藏。

 

<!--EndFragment-->

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
游庄
游庄
江湖只有他的大名,没有他的介绍。