EVM Gas、费用与代币 — 完整指南

EVM Gas、费用与代币 — 完整指南

本文面向开发者和部署者,深入讲解 EVM 链上的 gas 费用、单位体系、wei、原生代币、ERC-20 代币及其相互关系。涵盖从基础概念到 2025 年最新升级(EIP-4844、ERC-4337、Pectra、EIP-7702)的全面知识体系。
如果你曾混淆过 gas 单位与 wei,或者不确定 gas 限制到底是什么单位,本指南将为你彻底厘清这些概念。


目录


1. 原生代币 vs ERC-20 代币

在 EVM 生态系统中,"代币"这个词涵盖了两种截然不同的事物:原生代币ERC-20 代币。理解它们的区别是掌握 gas 费用体系的第一步。

代币类型层次图 EVM 区块链 原生代币(协议层) 内置于区块链协议,无合约地址 用于支付 Gas 费用 ETH 以太坊 BNB BSC POL/AVAX Polygon/Avalanche ERC-20 代币(合约层) 智能合约实现,遵循标准接口 需要原生代币支付交互 Gas USDT 6 位小数 USDC 6 位小数 UNI 18 位小数 DAI 18 位小数 包装代币(桥接原生与 ERC-20) 原生代币的 ERC-20 版本,1:1 锚定 使原生代币可参与 DeFi 协议 WETH ETH 包装 WBNB BNB 包装 WAVAX AVAX 包装 deposit() ERC-20 接口

1.1 原生代币

每条 EVM 兼容链有且仅有一个原生代币。它是链本身的"货币",在协议层面就存在,不依赖于任何智能合约。

核心特性:

  • 每条链唯一:以太坊的 ETH、BSC 的 BNB、Polygon 的 MATIC(现已更名为 POL)、Avalanche 的 AVAX、Arbitrum 的 ETH 等
  • 协议层存在:原生代币不是智能合约,它们内置于区块链协议中。当节点验证区块时,原生代币的转账逻辑由协议代码直接处理
  • 用于支付 Gas 费用:所有交易(包括与 ERC-20 代币、NFT、DeFi 协议交互)都必须用原生代币支付 gas 费用。这是 EVM 的硬性要求(在账户抽象出现前无法用其他代币替代,详见第 8.2 节
  • 简单价值转账:发送原生代币只需在交易的 value 字段中指定金额,不需要调用任何合约函数
  • 账户余额字段:每个以太坊账户(外部账户 EOA 或合约账户)都有一个内置的 balance 字段,记录其持有的原生代币数量。这个字段由协议直接维护

示例 — 发送原生代币(无需合约调用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 viem 发送 0.1 ETH — 直接在交易中指定 value,无需 data 字段
import { createWalletClient, http, parseEther } from 'viem';
import { mainnet } from 'viem/chains';

const client = createWalletClient({
chain: mainnet,
transport: http(),
});

const hash = await client.sendTransaction({
to: '0xRecipientAddress...',
value: parseEther('0.1'), // 0.1 ETH = 100000000000000000 wei
// 注意:没有 data 字段,因为不需要调用任何合约
});

1.2 ERC-20 代币

ERC-20 代币是基于智能合约的代币,遵循 ERC-20 标准 定义的统一接口。它们是 DeFi 生态系统的基石。

核心特性:

  • 智能合约实现:每个 ERC-20 代币是一个独立的智能合约,部署在链上,拥有自己唯一的合约地址。例如,以太坊主网上的 USDT 地址是 0xdAC17F958D2ee523a2206206994597C13D831ec7
  • 标准接口:所有 ERC-20 代币实现相同的接口函数:transfer()transferFrom()approve()balanceOf()totalSupply()allowance()
  • 通过合约函数转账:发送 ERC-20 代币需要调用代币合约的 transfer()transferFrom() 函数,这与发送原生代币完全不同
  • 内部余额映射:代币合约维护一个 mapping(address => uint256) 的映射表,记录每个地址持有的代币数量。这是合约状态,不是协议层余额
  • 需要原生代币支付 Gas:与 ERC-20 代币的任何交互(转账、授权、查询)都是链上交易,都需要用原生代币支付 gas 费用。这意味着即使你持有大量 USDT,如果没有 ETH(在以太坊上),你也无法转账

常见 ERC-20 代币示例:

代币 类型 说明
USDT 稳定币 锚定美元的稳定币
USDC 稳定币 Circle 发行的美元稳定币
WETH 包装代币 ETH 的 ERC-20 版本
WBNB 包装代币 BNB 的 ERC-20 版本
UNI 治理代币 Uniswap 治理代币
CAKE 治理代币 PancakeSwap 治理代币
LINK 功能代币 Chainlink 预言机代币
DAI 稳定币 去中心化稳定币

示例 — 发送 ERC-20 代币(需要合约调用):

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 viem 发送 100 USDT — 必须调用 USDT 合约的 transfer 函数
import { createWalletClient, http, encodeFunctionData, erc20Abi } from 'viem';

const hash = await client.sendTransaction({
to: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT 合约地址
value: 0n, // 不发送原生代币
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: ['0xRecipientAddress...', 100_000_000n], // 100 USDT (6位小数)
}),
});

1.3 包装原生代币(WETH、WBNB)

包装原生代币(Wrapped Native Token)是原生代币的 ERC-20 版本,始终保持 1:1 比例支撑。它们的存在解决了一个重要的接口兼容性问题。

为什么需要包装代币?

原生代币(如 ETH)不符合 ERC-20 标准接口。然而,DeFi 协议(DEX、借贷平台、流动性池)在设计时通常假定所有代币都遵循 ERC-20 接口。为了让原生代币能参与这些协议,就需要一个遵循 ERC-20 标准的"包装"版本。

各链的包装代币:

原生代币 包装代币 包装合约
以太坊 ETH WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
BSC BNB WBNB 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c
Polygon MATIC WMATIC 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270
Avalanche AVAX WAVAX 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7
Arbitrum ETH WETH 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1

包装(Wrap)过程:

  1. 用户向 WETH 合约发送原生 ETH(通过 deposit() 函数或直接发送)
  2. 合约接收并持有原生 ETH
  3. 合约铸造等量的 WETH(ERC-20 代币)给用户
  4. 结果:合约持有的 ETH = 所有流通的 WETH 总量

解包(Unwrap)过程:

  1. 用户调用 WETH 合约的 withdraw() 函数
  2. 合约销毁用户的 WETH
  3. 合约将等量的原生 ETH 发送回用户
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WETH 合约核心逻辑(简化版)
contract WETH {
mapping(address => uint256) public balanceOf;

// 包装:存入 ETH,铸造 WETH
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
}

// 解包:销毁 WETH,取回 ETH
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}

1.4 关键区别总结表

方面 原生代币 ERC-20 代币
存储位置 协议层余额(每个账户的 balance 字段) 智能合约内部映射(mapping(address => uint256)
转账方式 交易中的 value 字段 调用合约的 transfer()transferFrom() 函数
Gas 支付 是(仅原生代币可用于支付 gas) 否(必须用原生代币支付 gas)
合约地址 无(内置于链的协议层中) 有独立的合约地址
创建方式 链创世或协议共识 部署智能合约
标准接口 无统一标准 ERC-20 标准接口
授权机制 无需授权(直接发送) 需要 approve() 后才能被第三方使用
示例 ETH、BNB、MATIC、AVAX USDT、USDC、WETH、WBNB、UNI、CAKE

2. 理解 Wei — 最小单位

2.1 面额体系

就像人民币有"元"和"分"的面额体系一样,以太坊也有自己的面额体系。Wei 是最小的面额单位,就像"分"是人民币的最小单位一样 — 但区别在于 ETH 有 18 位小数,而人民币只有 2 位。

Wei 面额阶梯图 从 wei 到 ether 的可视化刻度 — 每级相差 10^3(蛇形升阶路径) wei 10^0 kwei 10^3 mwei 10^6 gwei 10^9 = Gas 价格单位 x10^3 szabo 10^12 finney 10^15 ether 10^18 = 用户可读单位 x10^3 x10^3 x10^3 x10^3 x10^3 = 常用单位(开发者需记住的三个) = 历史单位(很少使用)

完整的以太坊面额表:

单位 Wei 值 科学计数法 实际用途
wei 1 10^0 EVM 内部运算的基本单位
kwei (babbage) 1,000 10^3 很少使用
mwei (lovelace) 1,000,000 10^6 很少使用
gwei (shannon) 1,000,000,000 10^9 Gas 价格的标准单位
szabo 1,000,000,000,000 10^12 历史单位,很少使用
finney 1,000,000,000,000,000 10^15 历史单位,很少使用
ether 1,000,000,000,000,000,000 10^18 用户可读的标准单位

记忆要点: Wei 之于 ETH,就如同分之于元 — 只不过有 18 位小数而不是 2 位。
在日常使用中,你只需要记住三个单位:wei(EVM 内部)、gwei(gas 价格)、ether(用户界面)。

2.2 为什么需要 Wei

EVM 使用 wei 作为基本单位,这不是随意的设计决策,而是由底层技术决定的:

  • EVM 不支持浮点数:以太坊虚拟机的所有算术运算都是整数运算(uint256)。没有浮点数类型,没有小数点。这是为了保证确定性 — 不同硬件上的浮点运算可能产生不同结果,但整数运算始终一致
  • 18 位小数提供充足精度:18 位小数意味着你可以表示 0.000000000000000001 ETH 的精度,这对微交易和 DeFi 协议中的精密计算至关重要
  • 所有值以 wei 存储和计算:EVM 内部不存在"ETH"的概念,只有 wei。当钱包显示 "1 ETH" 时,EVM 实际存储和处理的是 1,000,000,000,000,000,000 wei(即 1e18
  • 避免精度损失:如果使用浮点数,0.1 + 0.2 可能等于 0.30000000000000004。在金融系统中,这种精度损失是不可接受的。使用整数 wei 运算则完全精确
1
2
3
4
// EVM 内部:全部是整数运算
uint256 balance = 1000000000000000000; // 这就是 "1 ETH"
uint256 half = balance / 2; // 500000000000000000 = 0.5 ETH
// 没有任何精度损失

2.3 常见转换及实际示例

基础转换关系:

1
2
1 ETH  = 1,000,000,000 gwei  = 1,000,000,000,000,000,000 wei
1 gwei = 1,000,000,000 wei

实际场景示例:

场景 ETH 值 Gwei 值 Wei 值
1 ETH 1 1,000,000,000 1,000,000,000,000,000,000
千分之一 ETH 0.001 1,000,000 1,000,000,000,000,000
Gas 价格 5 gwei 0.000000005 5 5,000,000,000
Gas 价格 30 gwei 0.00000003 30 30,000,000,000
简单转账费用 (5 gwei) 0.000105 105,000 105,000,000,000,000

代码中的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { parseEther, formatEther, parseGwei, formatGwei } from 'viem';

// 用户可读 → wei(发送交易前)
parseEther('1.0') // → 1000000000000000000n (BigInt)
parseEther('0.001') // → 1000000000000000n
parseGwei('5') // → 5000000000n

// wei → 用户可读(显示给用户前)
formatEther(1000000000000000000n) // → '1.0'
formatEther(1000000000000000n) // → '0.001'
formatGwei(5000000000n) // → '5'

// 常见错误:混淆 parseEther 和 parseGwei
parseEther('5') // 5 ETH = 5000000000000000000n ← 这不是 5 gwei!
parseGwei('5') // 5 gwei = 5000000000n ← 这才是 5 gwei

2.4 ERC-20 代币的小数位不一定是 18

这是一个极其重要的知识点。虽然 ETH 和大多数原生代币使用 18 位小数,但 ERC-20 代币的小数位由代币合约自行定义,各不相同。

常见代币及其小数位:

代币 小数位 1 个代币的最小单位值 说明
ETH (原生) 18 1,000,000,000,000,000,000 标准 18 位
BNB (原生) 18 1,000,000,000,000,000,000 标准 18 位
WETH 18 1,000,000,000,000,000,000 与 ETH 相同
UNI 18 1,000,000,000,000,000,000 标准 18 位
LINK 18 1,000,000,000,000,000,000 标准 18 位
DAI 18 1,000,000,000,000,000,000 标准 18 位
USDT 6 1,000,000 注意:不是 18 位!
USDC 6 1,000,000 注意:不是 18 位!
WBTC 8 100,000,000 注意:不是 18 位!

为什么小数位不同?

  • USDT/USDC 使用 6 位小数,因为它们锚定美元,而美元精度通常到分(2 位),6 位已经绰绰有余
  • WBTC 使用 8 位小数,因为比特币原生使用 8 位小数(1 BTC = 100,000,000 聪)

代码中必须处理不同小数位:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误示范:假设所有代币都是 18 位小数
const amount = parseEther('100'); // 100 * 10^18 — 对 USDT 来说这是天文数字!

// 正确做法:使用代币实际的小数位
import { parseUnits, formatUnits } from 'viem';

const decimals = await tokenContract.read.decimals(); // USDT 返回 6

parseUnits('100', 6); // 100 USDT = 100000000n (10^8)
parseUnits('100', 18); // 100 ETH = 100000000000000000000n (10^20)

formatUnits(100000000n, 6); // '100.0' USDT
formatUnits(100000000000000000000n, 18); // '100.0' ETH

关键提醒: 在编写涉及代币金额的代码时,始终调用代币合约的 decimals() 函数来获取正确的小数位数。假设小数位为 18 是 DeFi 开发中最常见的错误之一。


3. Gas — 计算单位

3.1 什么是 Gas?

Gas 是 EVM 中一个抽象的计量单位,用于衡量执行智能合约或交易所需的计算工作量。理解这一点至关重要:

  • Gas 不是代币:你不能"持有"gas,不能"转账"gas,不能在钱包中看到 gas 余额
  • Gas 不是货币:gas 没有市场价格,你无法在交易所买卖 gas
  • Gas 不是 ETH 的面额:gas 不是 wei、不是 gwei、也不是 ether。它们是完全不同的度量体系
  • Gas 是计算量的度量:可以将其理解为"CPU 周期"或"计算步骤" — 衡量 EVM 执行某个操作需要做多少工作

类比理解:

想象你在超市购物。每件商品都有一个"重量"(以克为单位),你推着一辆有载重限制的购物车:

  • 商品重量(克) 相当于 Gas 单位 — 衡量每个操作的"份量"
  • 每克价格(元/克) 相当于 Gas 价格(gwei) — 市场决定的单价
  • 总价(元) 相当于 Gas 费用(ETH/BNB) — 你实际支付的金额
  • 购物车载重限制 相当于 Gas 限制 — 你愿意承担的最大负荷

每个 EVM 操作码(opcode)都有一个固定的 gas 成本,由以太坊协议规定。这些成本反映了操作的计算复杂度和资源消耗。

3.2 各操作的 Gas 成本

EVM Gas 计量流程 Gas 如何逐操作码消耗 — 以 ERC-20 transfer 为例 初始 Gas 限制 65,000 gas 1. 基础交易费用 -21,000 gas 每笔交易固定消耗 剩余: 44,000 2. Calldata 编码 (函数签名+参数) -2,176 gas 68字节 x (16或4) gas/字节 剩余: 41,824 3. SLOAD: 读取发送者余额 (冷访问) -2,100 gas 首次存储槽读取 剩余: 39,724 4. SLOAD: 读取接收者余额 (冷访问) -2,100 gas 另一个存储槽 剩余: 37,624 5. SSTORE x2: 更新双方余额 -10,000 gas 2 x 5,000 (更新已有值) 剩余: 27,624 6. LOG3: 触发 Transfer 事件 -1,756 gas 事件日志 + 索引参数 剩余: 25,868 实际使用: ~39,132 gas + 其他操作码 (ADD, PUSH, JUMP等) 退还: ~25,868

以下是 EVM 中常见操作码的 gas 成本。这些值由以太坊黄皮书和后续 EIP 升级定义:

操作 Gas 成本 说明
ADD / SUB 3 简单算术运算(加法、减法)
MUL / DIV 5 乘法、除法运算
ADDMOD / MULMOD 8 模运算
EXP 10 + 50 x 字节数 指数运算,成本随指数大小增加
SHA3 (KECCAK256) 30 + 6 x 字长 哈希运算,用于映射键计算等
BALANCE 2,600 查询地址余额(EIP-2929 后冷访问)
SLOAD 2,100 从存储中读取一个 256 位的值(冷访问)
SLOAD(热访问) 100 同一交易中再次读取同一存储槽
SSTORE(新值) 20,000 将值写入之前为零的存储槽 — 最昂贵的常见操作
SSTORE(更新) 5,000 更新已有存储槽中的值
SSTORE(清零) 5,000 + 退还 将存储槽设为零(可获得 gas 退还)
LOG0 375 触发无索引事件
LOG1 750 触发带 1 个索引参数的事件
LOG2 1,125 触发带 2 个索引参数的事件
CALL 2,600 调用另一个合约(冷访问)
CREATE 32,000 部署新合约(不含合约代码 gas)
CREATE2 32,000 部署新合约到确定性地址
SELFDESTRUCT 5,000 销毁合约(已弃用)
PUSH0 2 EIP-3855 新增:压入零值,比 PUSH1 0x00 节省 1 gas
TLOAD 100 EIP-1153 新增:读取瞬态存储(详见第 8.4 节
TSTORE 100 EIP-1153 新增:写入瞬态存储
基础交易 21,000 每笔交易的最低消耗
交易数据(零字节) 4 / 字节 交易 calldata 中的零字节
交易数据(非零字节) 16 / 字节 交易 calldata 中的非零字节

关键观察: 存储操作(SSTORE)是最昂贵的。写入一个新存储槽的成本(20,000 gas)是简单加法(3 gas)的近 7,000 倍。这就是为什么 Solidity gas 优化的核心策略是减少存储写入。而 EIP-1153 引入的瞬态存储(TLOAD/TSTORE)以仅 100 gas 的成本提供了交易内临时数据存储的替代方案。

3.3 Gas 限制 vs 实际使用量

这是开发者最容易混淆的概念之一。在每笔交易中,有两个与 gas 相关的数值:

Gas 限制 (Gas Limit)

  • 由用户(或 dApp)在发送交易前设定
  • 表示"我愿意为这笔交易花费的最大 gas 量"
  • 是一个安全上限,防止意外消耗过多 gas
  • 钱包通常会自动估算并设置一个合理值

实际使用量 (Gas Used)

  • 交易执行完毕后才能确定
  • 表示交易实际消耗的 gas 量
  • 始终小于等于 Gas 限制
Gas 限制 vs 实际使用 — 三种场景 场景一:正常执行 实际使用 < Gas 限制 已使用: 65,000 退还: 35,000 Gas 限制: 100,000 交易成功 费用 = 65,000 x 5 gwei = 0.000325 BNB 场景二:恰好用完 实际使用 = Gas 限制 已使用: 100,000 (全部) Gas 限制: 100,000 交易成功(但无退还) 费用 = 100,000 x 5 gwei = 0.0005 BNB 场景三:Gas 不足 实际需求 > Gas 限制 全部耗尽: 50,000 / 需要 65,000 Gas 限制: 50,000 (不够!) 交易回滚 (Out of Gas) 费用 = 50,000 x 5 gwei (不退还!) = 0.00025 BNB 白白损失 关键要点 1. Gas 限制是安全上限,不是实际消耗。多余部分退还(场景一) 2. Gas 不足导致交易回滚,已消耗的 gas 费用不退还(场景三)— 这是最常见的 gas 陷阱 3. 钱包自动估算通常会在实际需要量基础上增加 20-30% 安全缓冲

三种情况:

情况 结果 费用
实际使用 < Gas 限制 交易成功,未使用的 gas 退还 只支付实际使用量的费用
实际使用 = Gas 限制 交易成功(恰好用完) 支付全部 gas 限制的费用
实际使用 > Gas 限制 交易回滚(revert) 所有 gas 被消耗,不退还,但状态变更被撤销

具体示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
场景:ERC-20 转账,gas 价格 5 gwei

设定 Gas 限制:100,000
实际使用 Gas:65,000
未使用 Gas:35,000(退还)

实际费用 = 65,000 x 5 gwei = 325,000 gwei = 0.000325 ETH
退还金额 = 35,000 x 5 gwei = 175,000 gwei = 0.000175 ETH

如果 Gas 限制设为 50,000(低于实际需要的 65,000):
→ 交易在执行到 50,000 gas 时耗尽并回滚
→ 所有 50,000 gas 的费用被扣除(0.00025 ETH)
→ 转账没有发生(状态未改变)
→ 你白白损失了 gas 费用

最佳实践: 不要把 gas 限制设得太低(会导致交易失败并损失 gas 费),也不要设得太高(虽然多余部分会退还,但有些特殊情况可能导致意外高消耗)。大多数钱包的自动估算会在实际需要量的基础上增加一个安全缓冲(通常 20-30%)。

3.4 为什么需要 Gas

Gas 机制不是可有可无的设计,它解决了区块链面临的根本性问题:

1. 停机问题(Halting Problem)

EVM 是图灵完备的虚拟机,这意味着它可以执行任意复杂的程序 — 包括无限循环。在计算理论中,不可能预先判断一个任意程序是否会终止(这就是"停机问题")。

Gas 机制优雅地解决了这个问题:每个操作消耗 gas,gas 限制封顶了总执行量。无论程序多复杂,gas 耗尽时执行必须停止。

1
2
3
4
5
6
7
// 没有 gas 机制,这个合约将永远运行
function infiniteLoop() public {
while (true) {
// 永远不会停止...
}
}
// 有了 gas 机制,执行到 gas 限制时自动停止并回滚

2. 防止资源滥用

网络中的每个验证节点都需要执行交易中的代码。如果没有执行成本,攻击者可以发送大量计算密集型交易来瘫痪网络(DoS 攻击)。Gas 费用使得此类攻击在经济上不可行。

3. 区块空间市场

每个区块有 gas 上限(以太坊目前约 3600 万 gas,Pectra 升级后提高至 3600 万)。当网络繁忙时,用户竞争有限的区块空间。Gas 价格机制创造了一个高效的市场:愿意支付更高 gas 价格的交易会被优先处理。

4. 激励验证者

验证者(或矿工)消耗真实的计算资源来执行交易。Gas 费用补偿他们的成本并提供利润激励,确保网络持续运行。

3.5 各交易类型的典型 Gas 使用量

以下数据基于实际链上统计的近似值,帮助你估算各种操作的成本:

交易类型 典型 Gas 使用量 按 5 gwei 计算(BSC) 按 30 gwei 计算(ETH)
简单 ETH/BNB 转账 21,000 0.000105 BNB 0.00063 ETH
ERC-20 代币转账 ~65,000 0.000325 BNB 0.00195 ETH
ERC-20 授权 (approve) ~46,000 0.00023 BNB 0.00138 ETH
DEX 兑换(简单路径) ~150,000 0.00075 BNB 0.0045 ETH
DEX 兑换(多跳路径) ~300,000 0.0015 BNB 0.009 ETH
添加流动性 ~200,000-300,000 0.001-0.0015 BNB 0.006-0.009 ETH
移除流动性 ~150,000-250,000 0.00075-0.00125 BNB 0.0045-0.0075 ETH
NFT 铸造(单个) ~100,000-150,000 0.0005-0.00075 BNB 0.003-0.0045 ETH
NFT 批量铸造 ~150,000-500,000 0.00075-0.0025 BNB 0.0045-0.015 ETH
ERC-20 合约部署 ~1,000,000-2,000,000 0.005-0.01 BNB 0.03-0.06 ETH
复杂合约部署 ~3,000,000-5,000,000 0.015-0.025 BNB 0.09-0.15 ETH

注意: 以上费用以原生代币计价。实际美元成本取决于原生代币的市场价格。BSC 上的交易通常比以太坊便宜 10-100 倍,这也是许多项目选择 BSC 或 Layer 2 的原因之一。


4. 交易费用 — 综合理解

现在我们已经了解了 gas(计算单位)和 wei(代币面额),让我们看看它们如何结合形成实际的交易费用。

4.1 传统费用模型(EIP-1559 之前)

在 2021 年以太坊伦敦升级(EIP-1559)之前,以及目前 BSC 等链仍在使用的模型中,费用计算非常直接:

1
交易费用 = 实际使用 Gas x Gas 价格
  • 实际使用 Gas:以 gas 单位计(纯数字,无面额)
  • Gas 价格:以 gwei 为单位(用户设定或钱包建议)
  • 交易费用:以原生代币为单位(ETH、BNB 等)

完整计算示例:

1
2
3
4
5
6
7
8
9
10
11
情境:在 BSC 上进行一笔 DEX 兑换

实际使用 Gas:200,000 gas 单位
Gas 价格:5 gwei

计算过程:
200,000 x 5 gwei = 1,000,000 gwei
1,000,000 gwei = 1,000,000 x 10^9 wei = 1,000,000,000,000,000 wei
1,000,000,000,000,000 wei = 0.001 BNB(因为 1 BNB = 10^18 wei)

最终费用:0.001 BNB

在这个模型中,用户设定一个 gas 价格,矿工优先处理出价更高的交易。这导致了一个问题:在网络拥堵时,用户需要不断提高出价来确保交易被包含,导致 gas 价格剧烈波动且难以预测。

4.2 EIP-1559 费用模型(以太坊伦敦升级后)

以太坊在 2021 年 8 月的伦敦升级中引入了 EIP-1559,彻底改变了费用机制。这个模型引入了两个核心概念:

1
交易费用 = 实际使用 Gas x (基础费 + 优先费)
EIP-1559 费用结构 基础费、优先费、销毁机制的完整流程 用户 设置 maxFeePerGas 设置 maxPriorityFee msg.value 交易费用计算 有效 Gas 价格 = baseFee + priorityFee 总费用 = gasUsed x 有效 Gas 价格 多余部分 (maxFee - 有效价格) x gasUsed 退还用户 退还差额 基础费 (销毁) gasUsed x baseFee 永久从 ETH 供应中移除 使 ETH 趋向通缩 优先费 (验证者) gasUsed x priorityFee 支付给区块验证者 激励打包交易 🔥 EIP-1559 销毁 验证者收入 示例: gasUsed=200,000 baseFee=25 gwei priorityFee=2 gwei maxFee=35 gwei 总费用=0.0054 ETH | 销毁=0.005 ETH | 验证者=0.0004 ETH | 退还=0.00335 ETH

基础费 (Base Fee):

  • 由协议根据前一个区块的利用率自动计算,用户无法设定
  • 如果前一个区块超过 50% 满,基础费上升(最多 12.5%);低于 50% 满,基础费下降
  • 基础费被销毁(burned):从 ETH 总供应中永久移除,不给任何人。这使得 ETH 在高使用率时可能成为通缩资产
  • 使 gas 价格更可预测 — 用户可以看到当前基础费并合理估算下一个区块的费用

优先费 / 小费 (Priority Fee / Tip):

  • 由用户自行设定
  • 支付给验证者(不被销毁)
  • 激励验证者将你的交易包含在区块中
  • 网络不繁忙时,很小的优先费(如 1-2 gwei)即可

最大费用 (Max Fee / maxFeePerGas):

  • 用户愿意支付的每 gas 单位最高总费用
  • 必须大于等于 基础费 + 优先费
  • 如果最大费用 > 实际需要(基础费 + 优先费),多余部分退还
EIP-1559 基础费调节机制 基础费如何随区块利用率自动调整(每次最多变化 12.5%) 基础费 (gwei) 区块编号 10 20 30 40 50 60 目标: 30 gwei 50% 55% 70% 85% 100% 60% 45% 30% 25% 20% 区块满,费用飙升 区块空闲,费用下降 基础费变化曲线 底部数字 = 区块利用率(目标 50%) >50%: 基础费上升 | <50%: 基础费下降

完整计算示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
情境:在以太坊上进行一笔 DEX 兑换

设定参数:
Gas 限制:250,000
最大费用 (maxFeePerGas):35 gwei
优先费 (maxPriorityFeePerGas):2 gwei

实际执行时:
基础费(协议设定):25 gwei
实际使用 Gas:200,000

实际费用计算:
有效 gas 价格 = 基础费 + 优先费 = 25 + 2 = 27 gwei
交易费用 = 200,000 x 27 gwei = 5,400,000 gwei = 0.0054 ETH

费用分配:
销毁部分 = 200,000 x 25 gwei = 5,000,000 gwei = 0.005 ETH(永久销毁)
验证者收入 = 200,000 x 2 gwei = 400,000 gwei = 0.0004 ETH

退还:
预锁定金额 = Gas 限制 x 最大费用 = 250,000 x 35 gwei = 8,750,000 gwei
实际费用 = 5,400,000 gwei
退还 = 8,750,000 - 5,400,000 = 3,350,000 gwei = 0.00335 ETH

4.3 BSC 费用模型

BSC(BNB Smart Chain)使用接近传统的简化费用模型,但有自己的特点:

  • Gas 价格极低:BSC 的典型 gas 价格为 3-5 gwei,而以太坊通常为 20-50 gwei 甚至更高
  • 区块时间短:BSC 约 3 秒一个区块(以太坊约 12 秒),更快的区块意味着更多的区块空间供应
  • 基本公式费用 = 实际使用 Gas x Gas 价格
  • BNB 代币单价远低于 ETH:即使 gas 单位数相同,以 BNB 计价的费用对应的美元成本也更低
1
2
3
4
5
6
7
8
9
10
11
BSC 示例:部署一个代币合约

实际使用 Gas:2,000,000
Gas 价格:5 gwei

费用 = 2,000,000 x 5 gwei = 10,000,000 gwei = 0.01 BNB
按 BNB = $600 计算:约 $6.00

同样的合约在以太坊上:
费用 = 2,000,000 x 30 gwei = 60,000,000 gwei = 0.06 ETH
按 ETH = $3,000 计算:约 $180.00

这就是为什么许多开发者和用户选择在 BSC 上部署和交互 — 成本差异可以达到 30 倍以上。

4.4 各链费用对比

各链 Gas 成本对比(简单转账) 2025 年近似参考值 — 水平条形图(美元计价) 以太坊 BSC Polygon Arbitrum Optimism Base $0.50 - $2.00 $0.01 - $0.02 $0.001 - $0.005 $0.01 - $0.05 $0.001 - $0.01 $0.001 - $0.01 注意:L2 链(Arbitrum, Optimism, Base)的费用已包含 L1 数据发布成本。EIP-4844 实施后 L2 费用大幅下降。 对数刻度展示 — 以太坊费用是 L2 链的 100-1000 倍

以下对比展示了不同 EVM 链的典型费用水平。需注意,gas 价格和代币价格都会波动,以下为 2025 年的近似参考值:

典型 Gas 价格 简单转账成本(原生代币) 简单转账成本(美元近似) DEX 兑换成本(美元近似)
以太坊 20-50 gwei 0.00042-0.00105 ETH $0.50-$2.00 $5-$30
BSC 3-5 gwei 0.000063-0.000105 BNB $0.01-$0.02 $0.10-$0.30
Polygon 30-50 gwei 0.00063-0.00105 MATIC $0.001-$0.005 $0.01-$0.05
Arbitrum 0.1-0.5 gwei 0.0000021-0.0000105 ETH $0.01-$0.05 $0.10-$0.50
Optimism 0.01-0.1 gwei 0.00000021-0.0000021 ETH $0.001-$0.01 $0.01-$0.10
Base 0.01-0.05 gwei 0.00000021-0.00000105 ETH $0.001-$0.01 $0.01-$0.10
Avalanche C-Chain 25-30 gwei 0.000525-0.00063 AVAX $0.01-$0.03 $0.10-$0.30

注意: Layer 2 链(Arbitrum、Optimism、Base)的 gas 价格看起来极低,但它们还有额外的 L1 数据发布费用(将交易数据提交到以太坊主网的成本)。上表中的美元近似值已包含这部分成本。自 EIP-4844(Dencun 升级)实施后,L2 的数据发布费用下降了约 90%。


5. "Gas" 的三种含义 — 常见混淆

这可能是本指南中最重要的一节。在日常对话、文档甚至代码注释中,"gas" 这个词经常被笼统使用,但实际上它可能指代三种完全不同的含义。混淆它们是 EVM 开发中最常见的错误来源之一。

5.1 术语辨析

术语 实际含义 单位 数量级 示例
Gas(单位/gas units) 计算预算 / 工作量度量 抽象无量纲单位 数万到数百万 "这笔交易使用了 200,000 gas"
Gas 价格 (gas price) 每 gas 单位的成本 gwei(= 10^9 wei) 个位到数百 "当前 gas 价格是 5 gwei"
Gas 费用 (gas fee) 以原生代币计的实际总成本 ETH / BNB / MATIC 小数点后数位 "我支付了 0.001 BNB 的 gas 费"

三者的数学关系:

1
2
3
4
5
6
Gas 费用 (ETH/BNB) = Gas 单位 x Gas 价格 (gwei) x 10^-9

具体例子:
Gas 费用 = 200,000 x 5 gwei x 10^-9
= 1,000,000 x 10^-9
= 0.001 BNB

常见混淆场景:

1
2
3
4
5
6
7
8
9
10
11
12
"我设置了 200,000 gas"
正确理解:我设置了 200,000 个 gas 单位的计算预算
错误理解:我要支付 200,000 BNB/ETH

"Gas 是 5"
→ 可能指:gas 价格是 5 gwei(最常见含义)
→ 也可能指:某个操作消耗 5 gas 单位
→ 需要上下文判断

"Gas 很贵"
→ 指的是:gas 费用(总成本)很高
→ 通常因为 gas 价格(gwei)较高导致

5.2 现实世界类比

用汽车加油的类比来理解这三个概念:

汽车加油 EVM Gas 关系
行驶距离(公里) Gas 单位 你的旅程有多长 / 计算有多复杂
油价(元/升) Gas 价格(gwei) 市场决定的单价
百公里油耗(升/百公里) 每操作 gas 成本 固定的消耗率
总油费(元) Gas 费用(ETH/BNB) 你实际从钱包中支付的金额
油箱容量(升) Gas 限制 你愿意承担的最大消耗量

延伸类比:

1
2
3
4
5
6
7
8
9
10
11
12
13
"我设置了 200,000 gas 的 gas limit"

= "我的油箱最多装 200,000 升油"
→ 不代表你会用完 200,000 升
→ 不代表你要付 200,000 元
→ 只代表如果旅程需要超过 200,000 升油,你就放弃这趟旅程
(交易回滚,但已消耗的油费不退还)

实际可能:
旅程只需要 65,000 升(实际使用 gas)
油价 5 元/升(gas 价格 5 gwei)
总油费 = 65,000 x 5 = 325,000 元(= 325,000 gwei = 0.000325 BNB)
剩余油箱容量 135,000 升(退还的 gas)

核心要点: 当你看到或设置一个 gas 相关的数值时,首先确认它是哪种含义(单位?价格?费用?)。200,000 gas 单位和 200,000 gwei 和 200,000 wei 是完全不同的数量级。


6. LayerZero 跨链 Gas — Enforced Options 上下文

跨链通信为 gas 概念增添了一层新的复杂性。在 LayerZero 协议中,一笔跨链交易涉及两条链上的执行,而用户只在源链上支付费用。理解这个机制对正确配置 NativeOFT 和跨链兑换至关重要。

LayerZero 跨链 Gas 流程 源链 → LayerZero 网络 → 目标链的完整 Gas 流转 源链 (BSC) 1. 用户调用 send() msg.value = LZ 总费用 2. 源链 Gas 消耗 ~300,000 gas x 5 gwei 3. LZ 费用拆分 协议费 + DVN 费 + 执行器费(含目标链 gas) 用户只需持有 BNB 不需要目标链原生代币 单链支付体验 LayerZero 网络 4. DVN 安全验证 消息真实性确认 5. 执行器准备 携带预付 gas 资金 enforced gas 限制检查 lzReceive: 200k, lzCompose: 500k 目标链 (Sepolia) 6. lzReceive 执行 OFT credit + WETH 解包 7. lzCompose 执行 DEX 兑换 (swap) 8. 目标链 Gas 消耗 700,000 gas (执行器支付) 9. 用户收到代币 兑换后的目标代币 到达用户目标链地址 用户总支付 = 源链 Gas 费 + LZ 跨链费(含目标链 Gas) 约 0.01-0.06 BNB

6.1 为什么跨链需要 Gas 限制

当 NativeOFT(原生 OFT 代币)通过 LayerZero 发送跨链消息时,整个流程如下:

  1. 源链:用户调用 send()initiateSwap() → 消耗源链 gas → 发出 LayerZero 消息
  2. LayerZero 网络:安全验证层(DVN)验证消息
  3. 目标链:LayerZero 执行器(Executor)调用目标合约 → 消耗目标链 gas

关键问题:目标链上的 gas 由谁支付?

答案是:由源链用户预先支付。用户在源链支付的 LZ 费用中包含了目标链执行的 gas 成本。LayerZero 执行器使用这些预付费用来在目标链上执行交易。

setEnforcedOptions 的作用就是告诉 LayerZero 协议:"在目标链上执行我的合约代码时,至少要分配 X 个 gas 单位。" 这确保了目标链上有足够的 gas 来完成操作,避免跨链交易在目标链上因 gas 不足而失败。

6.2 数值含义

让我们分解跨链兑换中的 gas 限制数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Enforced Options 配置示例:

lzReceive Gas 限制:200,000 gas 单位
├─ 用途:执行 OFT 代币 credit(记账)+ WETH 解包 + 原生代币转账
├─ 这是 gas 单位,不是 wei,不是 gwei,不是 BNB
├─ 按 BSC 目标链 5 gwei 计算实际成本:
│ 200,000 x 5 gwei = 1,000,000 gwei = 0.001 BNB
└─ 由 LayerZero 执行器在目标链上消耗

lzCompose Gas 限制:500,000 gas 单位
├─ 用途:在目标链 DEX 上执行代币兑换(swap)
├─ DEX 兑换比简单转账复杂得多,需要更多 gas
├─ 按 BSC 目标链 5 gwei 计算实际成本:
│ 500,000 x 5 gwei = 2,500,000 gwei = 0.0025 BNB
└─ 由 LayerZero 执行器在目标链上消耗

总目标链 gas 预算:
200,000 + 500,000 = 700,000 gas 单位
按 BSC 5 gwei 计算 约 0.0035 BNB

注意:这 0.0035 BNB 不是用户直接支付的,而是包含在
源链上支付的 LZ 总费用中。

重要区分: 200,000500,000gas 单位(抽象计算量),不是 wei 也不是 gwei。它们告诉 LayerZero 执行器"至少要为目标链操作分配这么多计算预算"。实际的费用(以目标链原生代币计)取决于目标链当时的 gas 价格。

6.3 LayerZero 费用工作原理

用户在源链上发送跨链交易时支付的 msg.value 包含多个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户在源链支付的 msg.value 构成:

msg.value = LZ 协议费 + 执行器费 + DVN 费

其中各部分:

┌─ LZ 协议费(Treasury Fee)
│ └─ LayerZero 协议收取的基础费用

├─ 执行器费(Executor Fee)
│ ├─ 目标链 lzReceive 的 gas 费用(200,000 gas 单位 x 目标链 gas 价格)
│ ├─ 目标链 lzCompose 的 gas 费用(500,000 gas 单位 x 目标链 gas 价格)
│ └─ 执行器的利润加成

└─ DVN 费(Decentralized Verifier Network Fee)
└─ 安全验证网络的服务费用

Enforced Options 与 quoteSend() 的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. 合约管理员调用 setEnforcedOptions()
→ 设置最低 gas 限制(如 lzReceive: 200,000, lzCompose: 500,000)

2. 用户(或前端 dApp)调用 quoteSend()
→ LayerZero 端点根据 enforced options 中的 gas 限制 + 当前目标链 gas 价格
→ 计算并返回所需的总费用(nativeFee)

3. 用户发送交易,msg.value = quoteSend() 返回的费用
→ 源链合约将费用转给 LayerZero 端点
→ LayerZero 执行器收到资金后在目标链执行

4. 如果 enforced options 设置的 gas 限制太低:
→ 目标链执行时 gas 不足 → 执行失败
→ 消息可能卡在 LayerZero 中需要手动重试

如果设置的 gas 限制过高:
→ 用户在源链预付更多费用(quoteSend 返回更高值)
→ 目标链实际可能用不完这么多 gas
→ 多余部分归执行器(不退还用户)

6.4 实际示例:BSC 测试网 → Sepolia 跨链兑换

以下是一笔完整的跨链兑换交易的费用分解,帮助你理解各部分如何组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
场景:从 BSC 测试网发起跨链兑换到 Sepolia 测试网

用户操作:
在 BSC 测试网上调用 initiateSwap()
发送 0.01 BNB 进行跨链兑换

=================================================
费用分解:
=================================================

1. 源链 Gas 费用(BSC 测试网)
├─ 操作:执行 initiateSwap() 函数
├─ 实际使用 Gas:~300,000 gas 单位
├─ Gas 价格:5 gwei
├─ 费用:300,000 x 5 gwei = 0.0015 BNB
└─ 支付方式:从用户 BNB 余额中扣除

2. LayerZero 跨链费用
├─ 通过 quoteSend() 预先计算
├─ 包含:
│ ├─ 目标链 lzReceive gas(200,000 单位)
│ ├─ 目标链 lzCompose gas(500,000 单位)
│ ├─ 执行器利润
│ ├─ DVN 验证费
│ └─ 协议费
├─ 总计:~0.01-0.05 BNB(取决于网络状况)
└─ 支付方式:作为 msg.value 发送

3. 目标链 Gas 费用(Sepolia)
├─ 由 LayerZero 执行器支付(从 LZ 费用中扣除)
├─ 用户不直接支付
├─ lzReceive 执行:~200,000 gas
└─ lzCompose 执行(DEX 兑换):~300,000-500,000 gas

=================================================
用户实际支付的总成本:
源链 gas 费用 + LayerZero 跨链费用
约 0.0015 + 0.01~0.05
约 0.01 - 0.06 BNB
=================================================

注意:
- 用户只需持有源链(BSC)的 BNB
- 不需要持有目标链(Sepolia)的 ETH
- 所有目标链费用已包含在 LZ 费用中
- 这正是跨链用户体验优化的核心:单链支付

7. 快速参考速查表

转换公式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=================================================
单位转换:
1 ETH = 10^9 gwei = 10^18 wei
1 gwei = 10^9 wei
1 BNB = 10^9 gwei = 10^18 wei(同样的面额体系)

费用计算:
Gas 费用 = 实际使用 Gas(gas 单位) x Gas 价格(gwei) x 10^-9

单位提醒:
Gas 价格:以 gwei 为单位(1 gwei = 10^9 wei)
Gas 限制:以 gas 单位为单位(不是 wei,不是 gwei,不是 ETH)
交易费用:以原生代币为单位(ETH、BNB、MATIC 等)
=================================================

常见数值参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
=================================================
交易类型的典型 Gas 使用量:
简单转账(ETH/BNB): 21,000 gas
ERC-20 代币转账: ~65,000 gas
ERC-20 授权 (approve):~46,000 gas
DEX 兑换(简单): ~150,000 gas
DEX 兑换(复杂路径): ~300,000 gas
合约部署: ~1,000,000-5,000,000 gas
NFT 铸造: ~100,000-200,000 gas

LayerZero 跨链 Enforced Options:
lzReceive: ~200,000 gas(enforced 最低值)
lzCompose: ~500,000 gas(DEX 兑换 enforced 最低值)

各链典型 Gas 价格:
以太坊: 20-50 gwei
BSC: 3-5 gwei
Polygon: 30-50 gwei
Arbitrum: 0.1-0.5 gwei
Base: 0.01-0.05 gwei
=================================================

ERC-20 授权流程

ERC-20 授权流程 approve() → transferFrom() 模式详解 用户 (Alice) 代币持有者 ERC-20 合约 USDT / USDC 等 DEX 路由合约 Uniswap / PancakeSwap 步骤一 approve(DEX, 1000) 用户授权 DEX 最多使用 1000 代币 消耗 ~46,000 gas allowance[Alice][DEX] = 1000 合约内部记录授权额度 步骤二 transferFrom(Alice, DEX, 500) DEX 从 Alice 账户转走 500 代币 消耗 ~65,000 gas | 剩余授权: 500 安全提示:避免授权 type(uint256).max(无限授权),建议只授权所需金额或使用 Permit2

代码示例(viem/ethers.js v6)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { parseEther, parseGwei, formatEther, formatGwei, parseUnits, formatUnits } from 'viem';

// =================================================
// 基础单位转换
// =================================================

// ETH / BNB → wei
parseEther('1.0') // 1000000000000000000n (1e18 wei)
parseEther('0.001') // 1000000000000000n (1e15 wei)
parseEther('0.000000005') // 5000000000n (5 gwei 对应的 wei 值)

// gwei → wei
parseGwei('5') // 5000000000n (5e9 wei)
parseGwei('30') // 30000000000n (30e9 wei)

// wei → 用户可读
formatEther(1000000000000000000n) // '1.0' (ETH/BNB)
formatEther(1000000000000000n) // '0.001'
formatGwei(5000000000n) // '5' (gwei)

// =================================================
// ERC-20 代币(注意不同小数位!)
// =================================================

// 18 位小数代币(WETH、UNI、DAI 等)
parseUnits('100', 18) // 100000000000000000000n
formatUnits(100000000000000000000n, 18) // '100.0'

// 6 位小数代币(USDT、USDC)
parseUnits('100', 6) // 100000000n
formatUnits(100000000n, 6) // '100.0'

// 8 位小数代币(WBTC)
parseUnits('1', 8) // 100000000n
formatUnits(100000000n, 8) // '1.0'

// =================================================
// Gas 费用计算
// =================================================

// 场景:计算交易费用
const gasUsed = 200_000n; // gas 单位(BigInt)
const gasPrice = parseGwei('5'); // 5 gwei = 5000000000n wei
const fee = gasUsed * gasPrice; // 1000000000000000n wei
console.log(formatEther(fee)); // '0.001' ETH/BNB

// 场景:EIP-1559 费用计算
const baseFee = parseGwei('25'); // 25 gwei
const priorityFee = parseGwei('2'); // 2 gwei
const effectiveGasPrice = baseFee + priorityFee; // 27 gwei
const totalFee = gasUsed * effectiveGasPrice;
console.log(formatEther(totalFee)); // '0.0054' ETH

// =================================================
// LayerZero gas 限制(注意:这是 gas 单位,不是 wei!)
// =================================================

// Enforced options 中的 gas 限制
const lzReceiveGas = 200_000; // 200,000 gas 单位 — 不是 wei!
const lzComposeGas = 500_000; // 500,000 gas 单位 — 不是 wei!

// 估算目标链实际费用
const targetGasPrice = parseGwei('5');
const lzReceiveFee = BigInt(lzReceiveGas) * targetGasPrice;
const lzComposeFee = BigInt(lzComposeGas) * targetGasPrice;
console.log('lzReceive 费用:', formatEther(lzReceiveFee), 'BNB'); // '0.001'
console.log('lzCompose 费用:', formatEther(lzComposeFee), 'BNB'); // '0.0025'

8. 现代 Gas 格局(2025)

以太坊生态系统在 2024-2025 年经历了多项重大协议升级,深刻改变了 Gas 费用的格局。本章涵盖截至 2025 年的最新变化。

8.1 EIP-4844:Proto-Danksharding 与 Blob 交易

背景

EIP-4844(又称 Proto-Danksharding)在 2024 年 3 月的 Dencun 升级 中被引入以太坊主网。它引入了一种全新的交易类型 — Type 3(blob 交易),专为 Layer 2 Rollup 的数据发布场景设计。

核心机制

在 EIP-4844 之前,L2 Rollup(如 Arbitrum、Optimism、Base)需要将交易数据作为 calldata 发布到以太坊主网,这是极其昂贵的,因为 calldata 永久存储在链上。

EIP-4844 引入了 blob(Binary Large Object)— 一种临时数据存储。Blob 数据由共识层处理,在约 18 天后自动被裁剪删除,不永久占用链上空间。

1
2
3
4
5
6
7
8
9
10
11
EIP-4844 之前(L2 数据发布):
数据存储方式:calldata(永久存储)
费用:按 calldata gas 计价(16 gas/非零字节)
L2 单笔交易成本:$0.10 - $1.00

EIP-4844 之后(L2 数据发布):
数据存储方式:blob(临时存储,~18天)
费用:独立的 blob gas 市场(与普通 gas 分开计价)
L2 单笔交易成本:$0.001 - $0.01

费用下降:约 90-99%

Blob Gas 市场

Blob 有自己独立的费用市场,与 EIP-1559 的普通 gas 市场并行运行:

  • 每个区块最多包含 6 个 blob(目标为 3 个)
  • 每个 blob 大小为 128 KB
  • Blob gas 有独立的 base fee,类似 EIP-1559 的基础费调节机制
  • 当 blob 空间利用率超过目标值时,blob gas 价格上升;反之下降
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用 viem 发送 blob 交易
import { createWalletClient, http, toBlobs, parseGwei } from 'viem';
import { mainnet } from 'viem/chains';

const client = createWalletClient({
chain: mainnet,
transport: http(),
});

// 构建 blob 交易(通常由 L2 排序器自动完成)
const hash = await client.sendTransaction({
to: '0xRollupInbox...',
// blob 相关参数
blobs: toBlobs({ data: batchData }), // Rollup 批次数据
maxFeePerBlobGas: parseGwei('10'), // blob gas 最大费用
// 普通 gas 参数仍然需要
maxFeePerGas: parseGwei('30'),
maxPriorityFeePerGas: parseGwei('2'),
});

对开发者的影响

  1. L2 费用大幅下降:如果你的 dApp 部署在 L2 上,用户体验显著改善
  2. L2 优先策略更加合理:L2 的成本优势进一步扩大
  3. 新的 gas 估算维度:需要同时考虑 execution gas 和 blob gas

8.2 账户抽象(ERC-4337)与 Gas 赞助

传统 EOA 的 Gas 痛点

在传统模型中,只有 EOA(外部拥有账户,即私钥控制的普通账户)可以发起交易,并且必须持有原生代币来支付 Gas。这导致了严重的用户体验问题:

  • 新用户需要先获取原生代币才能进行任何操作
  • 持有 ERC-20 代币但没有 ETH 的用户无法转账
  • 每条链都需要独立准备 Gas 费用

ERC-4337 账户抽象

ERC-4337 引入了一种无需协议层修改的账户抽象方案,通过以下核心组件改变了 Gas 支付模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ERC-4337 核心组件:

UserOperation (用户操作)
├─ 类似传统交易,但由智能合约钱包处理
├─ 包含 callData、gas 限制等
└─ 不需要用户直接持有 ETH

Bundler (打包器)
├─ 将多个 UserOperation 打包成一笔链上交易
├─ 由打包器支付链上 gas
└─ 从 Paymaster 或用户智能钱包中收回费用

EntryPoint (入口合约)
├─ 统一的链上入口,验证和执行 UserOperation
└─ 处理 gas 计量和费用结算

Paymaster (付费方) ← 关键创新
├─ 第三方合约,为用户代付 gas 费用
├─ 场景一:dApp 赞助用户的 gas(免 gas 体验)
├─ 场景二:用户用 ERC-20 代币支付 gas(如用 USDC 付 gas)
└─ 场景三:订阅制 gas(包月 gas 服务)

代码示例:使用 Paymaster 赞助 Gas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 构建一个由 Paymaster 赞助 gas 的 UserOperation
import { createSmartAccountClient } from 'permissionless';
import { toSimpleSmartAccount } from 'permissionless/accounts';
import { createPimlicoClient } from 'permissionless/clients/pimlico';

// 创建 Paymaster 客户端
const pimlicoClient = createPimlicoClient({
transport: http('https://api.pimlico.io/v2/...'),
});

// 创建智能账户客户端
const smartAccountClient = createSmartAccountClient({
account: await toSimpleSmartAccount({ /* 配置 */ }),
chain: mainnet,
bundlerTransport: http('https://bundler.example.com'),
// Paymaster 配置 — 用户无需持有 ETH
paymaster: pimlicoClient,
});

// 发送交易 — gas 由 Paymaster 赞助
const hash = await smartAccountClient.sendTransaction({
to: '0xTokenContract...',
data: encodeFunctionData({
abi: erc20Abi,
functionName: 'transfer',
args: [recipient, amount],
}),
// 注意:用户无需指定 value 来支付 gas
// Paymaster 会自动处理 gas 费用
});

对 Gas 格局的影响

方面 传统 EOA ERC-4337 智能账户
Gas 支付者 必须是交易发起者 可以是第三方 Paymaster
支付代币 仅限原生代币 可以是 ERC-20(通过 Paymaster)
新用户体验 需要先获取 ETH/BNB 可以零 Gas 入门
批量操作 每笔交易单独付 Gas 可批量打包,摊薄 Gas 成本

8.3 Pectra 升级与 EIP-7702

Pectra 升级概览(2025 年)

Pectra 是以太坊在 2025 年计划的重大升级,合并了 Prague(执行层)和 Electra(共识层)的改进。对 Gas 的关键影响包括:

  • 区块 Gas 限制提升:从约 3000 万提高到 3600 万,增加约 20% 的区块空间
  • blob 数量增加:每区块目标 blob 数从 3 个增加到 6 个,最大从 6 个增加到 9 个
  • EIP-7702:为 EOA 引入临时智能合约能力

EIP-7702:EOA 的智能化

EIP-7702 允许 EOA 在单笔交易中临时"委托"给一个智能合约代码。这意味着普通的 EOA 钱包可以在不迁移到智能合约钱包的情况下获得账户抽象的部分好处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EIP-7702 工作原理:

传统 EOA:
私钥 → 签名交易 → 发送交易 → 每笔单独执行
限制:不能批量操作,不能赞助 gas,不能自定义验证逻辑

EIP-7702 增强的 EOA:
私钥 → 签署授权(designation)→ EOA 临时拥有合约代码
├─ 可以在单笔交易中执行多个操作(batch)
├─ 可以使用赞助 gas(sponsored transactions)
├─ 交易结束后,EOA 恢复为普通账户
└─ 不需要部署新的智能合约钱包

Gas 影响:
├─ 批量操作减少总 gas:5 笔转账 → 1 笔批量交易
│ 传统:5 x 21,000 = 105,000 gas
│ EIP-7702 批量:~80,000 gas(节省约 24%)
├─ 与 ERC-4337 Paymaster 兼容
└─ 降低账户抽象的迁移成本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// EIP-7702 示例:EOA 临时委托给批量执行合约
import { createWalletClient, http } from 'viem';

const client = createWalletClient({
chain: mainnet,
transport: http(),
});

// 签署 EIP-7702 授权
const authorization = await client.signAuthorization({
contractAddress: '0xBatchExecutor...', // 批量执行合约地址
});

// 发送带授权的交易 — EOA 临时拥有合约能力
const hash = await client.sendTransaction({
authorizationList: [authorization],
to: client.account.address, // 发送给自己(触发委托代码)
data: encodeFunctionData({
abi: batchExecutorAbi,
functionName: 'executeBatch',
args: [
// 批量执行多个操作
[
{ target: token1, data: transfer1Data, value: 0n },
{ target: token2, data: transfer2Data, value: 0n },
{ target: dex, data: swapData, value: 0n },
],
],
}),
});

8.4 现代 Gas 优化操作码

EIP-1153:瞬态存储(Transient Storage)

Dencun 升级引入的 TLOAD 和 TSTORE 操作码提供了在同一交易内的临时存储,交易结束后自动清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 传统方式:使用 SSTORE 存储重入锁
// 写入成本:20,000 gas(首次),清除后可获退还
contract TraditionalLock {
uint256 private _locked;

modifier nonReentrant() {
require(_locked == 0, "重入");
_locked = 1; // SSTORE: 20,000 gas
_;
_locked = 0; // SSTORE: 5,000 gas + 退还
}
}

// 现代方式:使用瞬态存储
// 读写成本均为 100 gas,且无需清除
contract TransientLock {
// 使用 EIP-1153 瞬态存储
modifier nonReentrant() {
assembly {
if tload(0) { revert(0, 0) } // TLOAD: 100 gas
tstore(0, 1) // TSTORE: 100 gas
}
_;
assembly {
tstore(0, 0) // TSTORE: 100 gas
// 交易结束后自动清除,此行是显式重置
}
}
// 总成本:约 300 gas(传统方式约 25,000 gas)
}

EIP-3855:PUSH0 操作码

Shanghai 升级引入了 PUSH0,专门用于将零值压入栈中。此前需要使用 PUSH1 0x00(3 gas),现在只需 PUSH0(2 gas)。

1
2
3
4
5
6
7
8
9
// PUSH0 自动被 Solidity 0.8.20+ 使用
// 当你写如下代码时,编译器会使用 PUSH0:
uint256 x = 0; // 编译后使用 PUSH0 而非 PUSH1 0x00
function foo() public returns (uint256) {
return 0; // 编译后使用 PUSH0
}

// 注意:如果你的合约需要部署在不支持 PUSH0 的链上,
// 请使用 Solidity < 0.8.20 或设置 evm_version = "paris"

8.5 Flashbots、MEV 与 Gas

MEV(最大可提取价值)如何影响 Gas

MEV(Maximal Extractable Value)是验证者/搜索者通过重排、插入或审查交易来获取额外利润的能力。MEV 直接影响 Gas 市场:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MEV 与 Gas 的关系:

1. 套利机器人(Arbitrage)
├─ 监控 mempool 中的大额交易
├─ 在目标交易前后插入自己的交易
├─ 愿意支付极高的优先费来确保排序
└─ 推高短期 gas 价格

2. 三明治攻击(Sandwich Attack)
├─ 检测到用户的大额 DEX 兑换
├─ 在用户交易前买入(前跑)→ 推高价格
├─ 用户交易以更差的价格成交
├─ 在用户交易后卖出(后跑)→ 获利
└─ 攻击者的交易使用高优先费

3. 清算(Liquidation)
├─ 借贷协议中的抵押品不足时
├─ 清算者竞争执行清算
├─ 使用极高优先费确保优先执行
└─ 导致瞬时 gas 价格飙升

Flashbots 与 MEV 保护

Flashbots 是以太坊生态中最重要的 MEV 基础设施之一。它提供了 MEV 保护和更高效的区块空间拍卖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 通过 Flashbots 发送私有交易(避免被三明治攻击)
// 使用 viem + Flashbots RPC
import { createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';

// 使用 Flashbots Protect RPC 发送交易
const client = createWalletClient({
chain: mainnet,
transport: http('https://rpc.flashbots.net'), // Flashbots 私有 RPC
});

// 交易不进入公共 mempool,直接发给区块构建者
// 避免被 MEV 搜索者发现和攻击
const hash = await client.sendTransaction({
to: dexRouter,
data: swapData,
maxFeePerGas: parseGwei('30'),
maxPriorityFeePerGas: parseGwei('2'),
});

// 注意:Flashbots Protect 交易不保证一定被包含
// 如果你的交易被跳过,需要等待或增加优先费重试

8.6 Access Lists(EIP-2930)

EIP-2930 引入了 Access Lists(访问列表),允许交易预先声明将要访问的存储槽和合约地址。这可以降低冷访问的 Gas 成本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Access List 的 Gas 节省原理:

没有 Access List 时:
首次访问地址:2,600 gas(冷访问 CALL)
首次读取存储:2,100 gas(冷访问 SLOAD)

使用 Access List 时:
预声明地址:2,400 gas(Access List 成本)+ 100 gas(热访问 CALL)
预声明存储:1,900 gas(Access List 成本)+ 100 gas(热访问 SLOAD)

节省(每个地址): 2,600 - 2,500 = 100 gas
节省(每个存储槽):2,100 - 2,000 = 100 gas

适用场景:
├─ 跨合约调用密集的复杂交易
├─ 多次访问同一存储槽的操作
└─ L2 交易(减少 calldata 中的冷访问标记)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 viem 创建带 Access List 的交易
import { createPublicClient, http } from 'viem';

const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});

// 自动生成 Access List(通过 eth_createAccessList RPC)
const accessList = await publicClient.createAccessList({
to: '0xDEXRouter...',
data: swapData,
from: userAddress,
});

// 发送带 Access List 的交易(Type 1 交易)
const hash = await walletClient.sendTransaction({
to: '0xDEXRouter...',
data: swapData,
accessList: accessList.accessList, // 预声明访问的地址和存储槽
maxFeePerGas: parseGwei('30'),
maxPriorityFeePerGas: parseGwei('2'),
});

9. 开发者 Gas 优化实践

本章提供实用的 Solidity Gas 优化技巧,每个技巧都附带 Gas 节省的量化分析。

9.1 存储优化

1. 变量打包(Storage Packing)

EVM 的存储以 32 字节(256 位)为一个槽(slot)进行组织。如果多个小变量可以放入同一个槽中,就只需要一次 SSTORE/SLOAD 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 差:每个变量占用独立的存储槽(3 个槽 = 3 次 SSTORE)
contract BadPacking {
uint256 a; // 槽 0 — 32 字节
uint8 b; // 槽 1 — 虽然只用 1 字节,但独占 32 字节槽
uint256 c; // 槽 2 — 32 字节
uint8 d; // 槽 3 — 同上
// 写入 b 和 d 需要 2 次 SSTORE = 40,000 gas(首次写入)
}

// 好:小变量相邻排列,共享存储槽(2 个槽)
contract GoodPacking {
uint256 a; // 槽 0 — 32 字节
uint256 c; // 槽 1 — 32 字节
uint8 b; // 槽 2 — 与 d 共享这个槽
uint8 d; // 槽 2 — 与 b 打包在同一个槽
// 写入 b 和 d 只需要 1 次 SSTORE = 20,000 gas
// 节省: 20,000 gas
}

2. 使用 immutable 和 constant

1
2
3
4
5
6
7
8
9
10
11
12
13
// constant:编译时确定,嵌入字节码中(不占用存储槽)
uint256 constant MAX_SUPPLY = 1_000_000; // 读取成本:约 3 gas(PUSH)

// immutable:部署时确定,嵌入字节码中(不占用存储槽)
uint256 immutable deployTime;
constructor() {
deployTime = block.timestamp; // 部署后读取成本:约 3 gas
}

// 对比:普通状态变量
uint256 maxSupply = 1_000_000; // 读取成本:2,100 gas(冷 SLOAD)

// 节省:每次读取节省约 2,097 gas

3. 使用 mapping 替代数组进行查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 差:遍历数组查找 — O(n) 复杂度,gas 随数组长度增长
address[] whitelist;
function isWhitelisted(address user) public view returns (bool) {
for (uint i = 0; i < whitelist.length; i++) {
if (whitelist[i] == user) return true; // 每次迭代 ~200 gas
}
return false;
// 100 个元素 = ~20,000 gas
}

// 好:映射查找 — O(1) 复杂度,gas 恒定
mapping(address => bool) whitelist;
function isWhitelisted(address user) public view returns (bool) {
return whitelist[user]; // 固定 ~2,100 gas(冷)或 ~100 gas(热)
}

9.2 Calldata 与内存优化

1. 使用 calldata 替代 memory 用于只读外部函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 差:memory 会复制数据到内存(额外 gas)
function processData(uint256[] memory data) external {
// data 被从 calldata 复制到 memory
// 复制成本:~3 gas/字 + 内存扩展成本
for (uint i = 0; i < data.length; i++) {
// 处理...
}
}

// 好:calldata 直接从交易数据中读取(不复制)
function processData(uint256[] calldata data) external {
// data 直接从 calldata 读取,零复制成本
for (uint i = 0; i < data.length; i++) {
// 处理...
}
}
// 每个 uint256 元素节省约 60 gas

2. 缩短 revert 消息或使用自定义错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 差:长字符串 revert 消息占用大量 calldata 和部署字节码
require(balance >= amount, "InsufficientBalance: the user does not have enough tokens");
// 字符串存储在字节码中,增加部署成本

// 好:使用自定义错误(Solidity 0.8.4+)
error InsufficientBalance(uint256 available, uint256 required);

function transfer(address to, uint256 amount) external {
if (balanceOf[msg.sender] < amount) {
revert InsufficientBalance(balanceOf[msg.sender], amount);
}
// ...
}
// 节省:部署 gas 减少(更短的字节码),revert 时 gas 也更少
// 自定义错误使用 4 字节选择器 vs 动态字符串 ABI 编码

9.3 循环与批量操作

1. 缓存存储变量到本地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 差:每次循环都读取存储变量
function sumBalances(address[] calldata users) external view returns (uint256) {
uint256 total;
for (uint i = 0; i < users.length; i++) { // users.length 每次从 calldata 读取(还好)
total += balances[users[i]]; // balances 的 mapping 槽每次 SLOAD
}
return total;
}

// 好:缓存循环中不变的存储值
uint256 public feeRate; // 假设循环中需要用到

function applyFees(uint256[] calldata amounts) external view returns (uint256[] memory) {
uint256[] memory results = new uint256[](amounts.length);
uint256 cachedFee = feeRate; // 一次 SLOAD(2,100 gas),之后从栈读取(3 gas)
for (uint i = 0; i < amounts.length; i++) {
results[i] = amounts[i] * cachedFee / 10000;
// 如果不缓存,每次循环都要 SLOAD feeRate
}
return results;
}
// 100 次循环节省:99 x 2,000 = ~198,000 gas

2. unchecked 块用于已知安全的算术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Solidity 0.8+ 默认开启溢出检查(每次算术操作额外 ~30 gas)
function sum(uint256[] calldata data) external pure returns (uint256) {
uint256 total;
for (uint256 i = 0; i < data.length; i++) {
total += data[i]; // 含溢出检查
}
// 含 i++ 溢出检查
return total;
}

// 当你确定不会溢出时,使用 unchecked 节省 gas
function sum(uint256[] calldata data) external pure returns (uint256) {
uint256 total;
uint256 len = data.length;
for (uint256 i; i < len; ) {
total += data[i];
unchecked { ++i; } // i 不可能溢出 uint256
}
return total;
}
// 每次循环迭代节省约 80 gas(i++ 检查 + 可能的 total 检查)

9.4 编译器与部署优化

1. Solidity 优化器设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// hardhat.config.js / foundry.toml 中的优化器配置
module.exports = {
solidity: {
version: '0.8.24',
settings: {
optimizer: {
enabled: true,
runs: 200, // 预期合约被调用的次数
// runs 较小:部署更便宜,运行时稍贵
// runs 较大:部署更贵,运行时更便宜
// 一般推荐:200(平衡)或 1000(频繁调用的合约)
},
viaIR: true, // 通过 IR(中间表示)编译,可额外优化 5-15%
evmVersion: 'cancun', // 使用最新 EVM 版本以获取新操作码
},
},
};

2. 使用代理模式减少部署成本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 当需要部署大量相同逻辑的合约时(如工厂模式)
// 使用最小代理(EIP-1167 Minimal Proxy / Clone)

// 实现合约只部署一次
contract TokenImplementation {
function initialize(string memory name, uint256 supply) external {
// 初始化逻辑...
}
}

// 工厂通过克隆创建代理(部署成本极低)
import "@openzeppelin/contracts/proxy/Clones.sol";

contract TokenFactory {
address immutable implementation;

constructor(address _impl) {
implementation = _impl;
}

function createToken(string memory name, uint256 supply) external returns (address) {
// 克隆部署成本:约 40,000 gas(vs 完整部署 1,000,000+ gas)
address clone = Clones.clone(implementation);
TokenImplementation(clone).initialize(name, supply);
return clone;
}
}

10. 常见陷阱与调试

10.1 Gas 估算失败

场景一:eth_estimateGas 返回错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 问题:gas 估算失败通常意味着交易会 revert
try {
const gasEstimate = await publicClient.estimateGas({
to: contractAddress,
data: callData,
account: userAddress,
});
} catch (error) {
// 常见原因:
// 1. 合约 require 条件不满足(余额不足、未授权等)
// 2. 合约被暂停(paused)
// 3. 调用者没有权限
// 4. 参数无效(零地址、超出范围等)

// 调试方法一:使用 eth_call 获取详细错误
try {
await publicClient.call({
to: contractAddress,
data: callData,
account: userAddress,
});
} catch (callError) {
// callError 通常包含 revert 原因
console.error('Revert 原因:', callError.message);
// 例如: "ERC20: transfer amount exceeds balance"
// 例如: "Ownable: caller is not the owner"
}
}

场景二:Gas 估算返回异常高的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 问题:估算返回 30,000,000 gas(接近区块上限)
// 这通常意味着合约逻辑中有问题

const gasEstimate = await publicClient.estimateGas({
to: contractAddress,
data: callData,
account: userAddress,
});

if (gasEstimate > 5_000_000n) {
console.warn('Gas 估算异常高:', gasEstimate);
// 可能原因:
// 1. 合约中的无限循环或超大循环
// 2. 存储操作过多(批量操作超过合理范围)
// 3. 合约调用了会 revert 的外部合约,但没有正确处理
// 4. 估算器返回了区块 gas 上限(兜底值)
}

10.2 交易卡住与替换

交易在 mempool 中卡住

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 交易卡住通常是因为 gas 价格太低或 nonce 问题

// 方法一:使用相同 nonce 发送更高 gas 价格的替换交易
const stuckTxNonce = 42n; // 卡住的交易的 nonce

const replacementHash = await walletClient.sendTransaction({
to: originalTo,
data: originalData,
value: originalValue,
nonce: stuckTxNonce, // 关键:使用相同的 nonce
// 提高 gas 价格(至少提高 10%,推荐翻倍)
maxFeePerGas: parseGwei('60'), // 原来是 30
maxPriorityFeePerGas: parseGwei('5'), // 原来是 2
});

// 方法二:发送一笔空交易取消(使用相同 nonce)
const cancelHash = await walletClient.sendTransaction({
to: walletClient.account.address, // 发送给自己
value: 0n,
nonce: stuckTxNonce,
maxFeePerGas: parseGwei('60'),
maxPriorityFeePerGas: parseGwei('5'),
});

Nonce 间隙问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 问题:nonce 3 的交易失败,但 nonce 4、5 已经发出
// 结果:nonce 4、5 卡住,因为 nonce 3 没有被确认

// 检查当前 nonce
const confirmedNonce = await publicClient.getTransactionCount({
address: userAddress,
blockTag: 'latest',
});

const pendingNonce = await publicClient.getTransactionCount({
address: userAddress,
blockTag: 'pending',
});

console.log('已确认 nonce:', confirmedNonce);
console.log('待处理 nonce:', pendingNonce);

// 如果 confirmedNonce < pendingNonce,说明有交易卡住
// 需要从 confirmedNonce 开始,按顺序重发或取消交易

10.3 合约交互陷阱

陷阱一:approve 之前忘记检查现有授权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 问题:某些代币(如 USDT)不允许从非零值直接 approve 到另一个非零值
// 必须先 approve 为 0,再 approve 新值

// 检查现有授权
const currentAllowance = await publicClient.readContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'allowance',
args: [userAddress, spenderAddress],
});

if (currentAllowance > 0n) {
// 先将授权设为 0
await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, 0n],
});
}

// 然后设置新的授权
await walletClient.writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, newAmount],
});

陷阱二:EIP-1559 交易中 maxFeePerGas 设置过低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 问题:baseFee 在你发送交易和被打包之间可能上升
// 如果 maxFeePerGas < baseFee,交易无法被包含

// 安全做法:查询当前 baseFee 并预留缓冲
const block = await publicClient.getBlock();
const currentBaseFee = block.baseFeePerGas!;

// 预留 2 倍缓冲(baseFee 每区块最多上升 12.5%,
// 2 倍缓冲可以覆盖约 6 个区块的最大上升)
const safeMaxFee = currentBaseFee * 2n + parseGwei('2'); // 2 gwei 优先费

const hash = await walletClient.sendTransaction({
to: recipient,
value: parseEther('1'),
maxFeePerGas: safeMaxFee,
maxPriorityFeePerGas: parseGwei('2'),
});
// 注意:多余的 maxFee 会退还,所以设高一些只是锁定更多资金,
// 实际不会多收费

陷阱三:在 L2 上忽略 L1 数据费用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 问题:在 Arbitrum/Optimism/Base 上,交易费用包含两部分:
// 1. L2 执行费(通常很低)
// 2. L1 数据发布费(可能是主要成本)

// 使用 viem 获取 L2 交易的完整费用估算
import { optimism } from 'viem/chains';

// Optimism 特定:估算 L1 数据费用
const l1Fee = await publicClient.readContract({
address: '0x420000000000000000000000000000000000000F', // L1Block 预编译
abi: gasPriceOracleAbi,
functionName: 'getL1Fee',
args: [serializedTx], // 序列化的交易数据
});

const l2GasCost = gasUsed * l2GasPrice;
const totalCost = l2GasCost + l1Fee;

console.log('L2 执行费:', formatEther(l2GasCost));
console.log('L1 数据费:', formatEther(l1Fee));
console.log('总费用:', formatEther(totalCost));

// 注意:EIP-4844 实施后,L1 数据费用大幅下降
// 因为 L2 现在使用 blob 而非 calldata 发布数据

交易生命周期

交易生命周期 从提交到打包的完整流程 1. 构建交易 设置 to, value, data 设置 maxFeePerGas, gasLimit 2. 签名 私钥签署交易数据 生成 v, r, s 签名字段 3. 广播到网络 通过 RPC 发送到节点 节点验证基本有效性 (nonce, 余额) 4. 内存池 (Mempool) 交易在此等待被验证者/区块构建者选中 按有效 gas 价格(优先费)排序 — 出价越高,优先级越高 注意:公共 mempool 对所有人可见 — MEV 搜索者在此寻找套利机会 Flashbots 私有池 绕过公共 mempool 直接发给区块构建者 避免 MEV 攻击 5. 区块构建与验证 验证者/区块构建者选择交易打包成区块 执行交易、计算状态根、生成收据(包含实际 gasUsed) 6a. 交易成功 状态变更生效 扣除: gasUsed x effectiveGasPrice 6b. 交易失败 (Revert) 状态变更回滚 Gas 费仍被扣除!已消耗 gas 不退还 后续区块确认 → 最终性 交易失败也被记录在链上

参考资料

核心规范

开发者资源

工具