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 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' ), });
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 import { createWalletClient, http, encodeFunctionData, erc20Abi } from 'viem' ;const hash = await client.sendTransaction ({ to : '0xdAC17F958D2ee523a2206206994597C13D831ec7' , value : 0n , data : encodeFunctionData ({ abi : erc20Abi, functionName : 'transfer' , args : ['0xRecipientAddress...' , 100_000_000n ], }), });
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)过程:
用户向 WETH 合约发送原生 ETH(通过 deposit() 函数或直接发送)
合约接收并持有原生 ETH
合约铸造等量的 WETH(ERC-20 代币)给用户
结果:合约持有的 ETH = 所有流通的 WETH 总量
解包(Unwrap)过程:
用户调用 WETH 合约的 withdraw() 函数
合约销毁用户的 WETH
合约将等量的原生 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' ;parseEther ('1.0' ) parseEther ('0.001' ) parseGwei ('5' ) formatEther (1000000000000000000n ) formatEther (1000000000000000n ) formatGwei (5000000000n ) parseEther ('5' ) parseGwei ('5' )
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 const amount = parseEther ('100' ); import { parseUnits, formatUnits } from 'viem' ;const decimals = await tokenContract.read .decimals (); parseUnits ('100' , 6 ); parseUnits ('100' , 18 ); formatUnits (100000000n , 6 ); formatUnits (100000000000000000000n , 18 );
关键提醒: 在编写涉及代币金额的代码时,始终 调用代币合约的 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 发送跨链消息时,整个流程如下:
源链 :用户调用 send() 或 initiateSwap() → 消耗源链 gas → 发出 LayerZero 消息
LayerZero 网络 :安全验证层(DVN)验证消息
目标链 :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,000 和 500,000 是 gas 单位 (抽象计算量),不是 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' ;parseEther ('1.0' ) parseEther ('0.001' ) parseEther ('0.000000005' ) parseGwei ('5' ) parseGwei ('30' ) formatEther (1000000000000000000n ) formatEther (1000000000000000n ) formatGwei (5000000000n ) parseUnits ('100' , 18 ) formatUnits (100000000000000000000n , 18 ) parseUnits ('100' , 6 ) formatUnits (100000000n , 6 ) parseUnits ('1' , 8 ) formatUnits (100000000n , 8 ) const gasUsed = 200_000n ; const gasPrice = parseGwei ('5' ); const fee = gasUsed * gasPrice; console .log (formatEther (fee)); const baseFee = parseGwei ('25' ); const priorityFee = parseGwei ('2' ); const effectiveGasPrice = baseFee + priorityFee; const totalFee = gasUsed * effectiveGasPrice;console .log (formatEther (totalFee)); const lzReceiveGas = 200_000 ; const lzComposeGas = 500_000 ; const targetGasPrice = parseGwei ('5' );const lzReceiveFee = BigInt (lzReceiveGas) * targetGasPrice;const lzComposeFee = BigInt (lzComposeGas) * targetGasPrice;console .log ('lzReceive 费用:' , formatEther (lzReceiveFee), 'BNB' ); console .log ('lzCompose 费用:' , formatEther (lzComposeFee), 'BNB' );
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 import { createWalletClient, http, toBlobs, parseGwei } from 'viem' ;import { mainnet } from 'viem/chains' ;const client = createWalletClient ({ chain : mainnet, transport : http (), }); const hash = await client.sendTransaction ({ to : '0xRollupInbox...' , blobs : toBlobs ({ data : batchData }), maxFeePerBlobGas : parseGwei ('10' ), maxFeePerGas : parseGwei ('30' ), maxPriorityFeePerGas : parseGwei ('2' ), });
对开发者的影响
L2 费用大幅下降 :如果你的 dApp 部署在 L2 上,用户体验显著改善
L2 优先策略更加合理 :L2 的成本优势进一步扩大
新的 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 import { createSmartAccountClient } from 'permissionless' ;import { toSimpleSmartAccount } from 'permissionless/accounts' ;import { createPimlicoClient } from 'permissionless/clients/pimlico' ;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 : pimlicoClient, }); const hash = await smartAccountClient.sendTransaction ({ to : '0xTokenContract...' , data : encodeFunctionData ({ abi : erc20Abi, functionName : 'transfer' , args : [recipient, amount], }), });
对 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 import { createWalletClient, http } from 'viem' ;const client = createWalletClient ({ chain : mainnet, transport : http (), }); const authorization = await client.signAuthorization ({ contractAddress : '0xBatchExecutor...' , }); 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 import { createWalletClient, http } from 'viem' ;import { mainnet } from 'viem/chains' ;const client = createWalletClient ({ chain : mainnet, transport : http ('https://rpc.flashbots.net' ), }); const hash = await client.sendTransaction ({ to : dexRouter, data : swapData, maxFeePerGas : parseGwei ('30' ), maxPriorityFeePerGas : parseGwei ('2' ), });
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 import { createPublicClient, http } from 'viem' ;const publicClient = createPublicClient ({ chain : mainnet, transport : http (), }); const accessList = await publicClient.createAccessList ({ to : '0xDEXRouter...' , data : swapData, from : userAddress, }); 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 module .exports = { solidity : { version : '0.8.24' , settings : { optimizer : { enabled : true , runs : 200 , }, viaIR : true , evmVersion : 'cancun' , }, }, };
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 try { const gasEstimate = await publicClient.estimateGas ({ to : contractAddress, data : callData, account : userAddress, }); } catch (error) { try { await publicClient.call ({ to : contractAddress, data : callData, account : userAddress, }); } catch (callError) { console .error ('Revert 原因:' , callError.message ); } }
场景二:Gas 估算返回异常高的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const gasEstimate = await publicClient.estimateGas ({ to : contractAddress, data : callData, account : userAddress, }); if (gasEstimate > 5_000_000n ) { console .warn ('Gas 估算异常高:' , gasEstimate); }
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 const stuckTxNonce = 42n ; const replacementHash = await walletClient.sendTransaction ({ to : originalTo, data : originalData, value : originalValue, nonce : stuckTxNonce, maxFeePerGas : parseGwei ('60' ), maxPriorityFeePerGas : parseGwei ('5' ), }); 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 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);
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 const currentAllowance = await publicClient.readContract ({ address : tokenAddress, abi : erc20Abi, functionName : 'allowance' , args : [userAddress, spenderAddress], }); if (currentAllowance > 0n ) { 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 const block = await publicClient.getBlock ();const currentBaseFee = block.baseFeePerGas !;const safeMaxFee = currentBaseFee * 2n + parseGwei ('2' ); const hash = await walletClient.sendTransaction ({ to : recipient, value : parseEther ('1' ), maxFeePerGas : safeMaxFee, maxPriorityFeePerGas : parseGwei ('2' ), });
陷阱三:在 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 import { optimism } from 'viem/chains' ;const l1Fee = await publicClient.readContract ({ address : '0x420000000000000000000000000000000000000F' , 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));
交易生命周期
交易生命周期
从提交到打包的完整流程
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 不退还
后续区块确认 → 最终性
交易失败也被记录在链上
参考资料
核心规范
开发者资源
工具