为什么需要GAS?GAS的设计理念
说到GAS费,就不得不提到比特币和以太坊的区别:比特币系统中用到的脚本语言是非常简单的,甚至连专门的名字都没有,它就叫比特币脚本语言(bitcoin scripting language)。
而我们知道,以太坊是一个图灵完备的虚拟机,理论上可以执行无限循环,以太坊当中用的智能合约的语言是Solidity,比比特币脚本语言要复杂的多,Solidity是一门图灵完备的语言。
如果不加限制,恶意用户可以提交永远不会停止的程序,使整个网络陷入瘫痪。
而比特币的脚本语言不支持循环,所以有很多功能比特币脚本语言是实现不了的,这样的设计是有其用意的,不支持循环就不会有死循环,就不用担心停机问题。
当一个以太坊的节点收到一个对智能合约调用怎么知晓其是否会导致死循环呢?
因为合约之间是可以相互调用,并且可以执行循环的,所以在事实上,节点是无法预知其是否会导致死循环的,该问题是一个停机问题,而停机问题不可解。
因此,以太坊引入GAS机制将该问题扔给了发起交易的账户。以太坊规定,执行合约中指令需要收取GAS,并且由发起交易的人进行支付。也就是靠GAS的机制来防止程序陷入死循环。
Gas 为每个操作定价,一旦用户的 Gas 消耗完毕,虚拟机就会强制中断执行,防止滥用资源。
可以说GAS机制是以太坊可持续运行的核心机制之一。
GAS机制
GAS 用来测量一个操作或一组操作需要执行多少"工作"量: 例如,计算一个 Keccak256 加密哈希函数,每次计算哈希时需要 30 个gas,再加上每 256 位被哈希的数据多花费 6 个gas。
在以太坊平台上通过交易或合约执行的每个操作都会消耗一定数量的gas,而需要更多计算资源的操作比需要少量计算资源的消耗的gas更多。
我们可以在这个链接查询每个操作需要的具体gas:www.evm.codes/
gas之所以重要,是因为它有助于确保提交到网络的交易支付适当的费用。
通过要求交易为其执行的每个操作(或合约执行)付费,可以确保网络不会因执行大量对任何人都没有价值的密集工作而陷入僵局。这与比特币交易费用的策略不一样,后者仅基于交易的千字节大小。由于以太坊允许任意复杂的计算机代码运行,即使是很短的代码也会可能导致大量计算工作(例如调用另一个逻辑复杂的合约)。因此,直接衡量工作量很重要,而不是仅仅根据交易或合约的长度来确定费用。
GAS计算的演进
在2021 年 8 月 5 日(伦敦升级)之前,以太坊的GAS费计算机制是简单粗暴的乘法和按价格排序机制。
用户设置手续费预算:指定 gas limit 和 gas price (发起者余额需要大于二者乘积)
竞拍方式:矿工把区块空间(资源)依次给最高的出价者(gas price)
矿工收益 = 用户手续费用 = gas 消耗(< gas limit) * gas price单价
这样会导致一些问题:
-
ETH的优先级无法得到保证。有人可以在链下给矿工支付交易费 (或者在链上以其他资产结算,比如DAI、USDC等) ,然后以0 gas费把他们的交易打包到链上 。这会导致ETH无法成为以太坊网络的首要货币。
-
Gas 价格波动剧烈,不可预测。在网络拥堵时,gas price 会瞬间暴涨。用户常常要盲目设置高价,猜猜多少才够快。用户不清楚设置多少 gas price 合适,要么失败(gas 太低),要么浪费(gas 太高)。
-
容易造成费用浪费。比如用户设置了 200 Gwei,但其实 80 Gwei 就能打包,差价被矿工“吃掉”。
-
Gas 战(Gas War)频繁。特别是抢 NFT 或热门 DeFi 操作时,用户互相抬价,gas price 飙升数百倍甚至更高。
-
无法实现 ETH 通缩。所有交易费都进入矿工手中,没有 ETH 被销毁,通胀压力难以缓解。
EIP-1559
目前以太坊上 Gas 费用的计算方式,主要基于 EIP-1559 提案,它在 2021 年 8 月 5 日伦敦硬分叉中引入,并且已经成为主流共识机制中的基础 Gas 定价模型。
相比于之前的方案,该提案对原有的GasPrice进行了拆分,变成了 Max priority fee 和 Max fee。
要了解这么做的原因,必须了解 EIP1559 下新的 GAS 费收取机制。我们刚刚提到了在 EIP1559 之前,矿工挖矿不仅会获得挖出新区块奖励,还会获得这个区块内所有的交易手续费。用户为一笔交易所指定的 gasPrice * gasUsed 会全部给矿工,作为额外的奖励。
而EIP-1559中:
以太坊系统指定了一个 Base fee,所有交易都会燃烧掉数量为 Base fee * gasUsed 的 ETH,只有 Max priority fee * gasUsed 才会作为奖励给到矿工。
也就是说如果用户指定的 Max fee > Base fee + Max priority fee,那么多出来的那部分会返还给用户。
如果 Max fee > 但是 Max fee < Base fee + Max priority fee,矿工也可能会打包交易,从而获取部分的 priority fee 奖励。
因此用户在选择 Max fee 时,实际上要同时考虑 Base fee 和 Max priority fee 这两个费用。
这样看起来有点绕,我们梳理下。
原来的Gas费是gasPrice * gasUsed,全部给到矿工。也就是说用户只需要设置用多少gas和gas的价格就行。
现在是:用户手续费预算需要设置三个值:
- gas limit 用户使用gas的上限
- max fee 用户愿意支付的每个gas的最大费用
- max priority fee 用户愿意为优先级支付的每个gas的最大费用
而上面提到的Base fee是不需要用户手动去设置的,用户也无法手动设置,Base Fee是系统自动设定,用户不可修改的。
Base Fee的设计初衷用一句话就可以描述,就是尽量让目标区块使用率设为 50%,让网络在“半满”状态下运行,避免极端拥堵。
所以影响Base fee的唯一因素就是上一个区块打包的整体 gas limit 是否使用超过了一半。如果超过了一半,就提升下一个区块的 Base Fee,最多提升 12.5%;如果没有超过一半,就减少下一个区块的 Base Fee,最多减少 12.5%。
对用户来说:用户手续费用 = gasUsed * (base fee + priority fee)。注意 gasUsed需要小于gas limit。
对以太坊官方来说:燃烧掉 = base fee * gasUsed
对矿工来说: 矿工收益 = priority fee * gas used,其中priority fee = min(max fee - base fee, max priority fee)
EIP-1559的优势
- 交易费用更可预测,用户不再需要盲目出高价
- 引入 ETH 销毁机制,BaseFee 被销毁,形成通缩压力,且燃烧掉的basefee必须是以太币,确保了ETH的优先级。
- 网络更加稳定,价格更平滑,减少价格波动,基于区块利用率调整 BaseFee,让矿工不再能哄抬gas价格。
- Tip 激励矿工/验证者,避免完全免费打包导致冷漠行为。
对单个用户来说,举一个具体的例子:
假设当前:
- Base fee = 30 Gwei
- max priority fee = 2 Gwei
- 用户设置 max fee = 50 Gwei
- 实际交易使用了 21,000 gas
那么:
- 实际支付的 Priority Fee = min(2 Gwei, 50 - 30) = 2 Gwei
- 每个gas Gas Price = 30 + 2 = 32 Gwei
- 总费用 = 21,000 × 32 Gwei = 672,000 Gwei = 0.000672 ETH
其中:
- BaseFee 部分(30 × 21,000)= 被销毁
- priority 部分(2 × 21,000)= 给矿工
EIP-1559下的优先级博弈
对于一些跑交易的同学来说,重要的不是 gas 费多少,而是如何跑在对手前面。
对于矿工而言,交易设置多少的 Max fee 其实并不重要,因为矿工并不能因此而得到直接的好处。重要的是,他能从这笔交易里拿到多少:Min( Max fee - Base fee, Max priority fee)。
下面举个例子:
已知下一个区块的 Base fee 为 30。 在一笔对手的交易中,Max fee 为 32,Max priority fee 为 2,而你的交易 Max fee 为 35 , Max priority fee 为 1,这样的情况下你的交易能优先被矿工打包吗?
虽然看起来你的交易支付的 gas 更多,但实际上矿工会优先选择对手的交易。
因为矿工能在对手的交易中提取 Min( 32 - 30, 2) = 2 的价值,而从你的交易中只能提取 Min( 35 - 30, 1) = 1 的价值。
另外还有一个比较特殊的情况,目前 EIP1559 下区块的 gas limit 为 3000 多万,如果你的交易 gas limit 太多(比如 1000 多万)而矿工可以从中提取的价值不够,矿工同样不会为了你而丢弃其他可提取价值高的交易。因此 gas limit 低其本身就是一种优势。
总结
- 智能合约越复杂 用来完成运行就需要越多Gas
- 用户指定的 gas price / priority fee, 决定交易的排序。
- gas limit > gas used 交易才能顺利执行, 否则出现 out of gas , 则交易回滚。
- 执行结束, gas limit 余下的部分不扣费用。
GAS 误区澄清
误区一:视图函数完全不消耗 Gas
在 本地调用(callStatic) 时不消耗 gas,但在链上通过合约内部调用 view/pure 函数时,仍计算 gas 消耗(如 view 函数里有复杂逻辑),只要执行了计算逻辑,都会消耗gas,只是view函数的gas price 为0。
误区二:estimateGas() 的值就等于实际上链消耗
estimateGas() 是模拟执行的上限估计,不包含 baseFee 变动、block limit 动态等真实环境影响。
建议:总是留 10-20% 的 buffer。
误区三:gasPrice 设置再低也能打包,只是排队慢
如果你设置的 maxFeePerGas(EIP-1559)低于当前 baseFee,交易会被直接淘汰,不会进入 mempool交易池。
误区四:失败交易不会消耗 gas
即使交易失败(revert),之前执行过的 opcode 所消耗的 gas 不会退还,除非主动触发 INVALID 或 OUT OF GAS 等特殊回退逻辑。
误区五:函数越复杂,gas 一定越高
函数结构复杂不等于执行成本高。高 gas 通常由:
- 多次 SSTORE(修改存储)
- 创建合约(CREATE、CREATE2)
- 重入调用等递归消耗引起
误区六:部署成本主要看代码行数
部署成本由字节码大小决定,而不是代码行数。 优化手段:
- 精简 unused imports
- 减少 inline assembly
- 避免无效 constructor 参数复制
误区七:修改 storage 比 memory 消耗略高
SSTORE 消耗远远高于 memory,例如:
- SLOAD: 2100 gas
- SSTORE 第一次写入:20,000 gas
- MSTORE: 仅需 3 gas
误区八:Layer 2 没有 gas 问题
L2(如 OptimismArbitrum)有更低 gas 成本,但仍需优化:
- calldata 占用影响 L1 posting 成本;
- 热路径函数仍需关注 storage 写入;
- Sequencer 有打包偏好(费用排序);
GAS优化技巧
GAS优化是一个非常复杂和宏大的话题,这篇文章是无法详尽的介绍到所有的GAS优化的技巧的。(如果大家对GAS优化技巧感兴趣,可以单开一篇单独介绍)
但是这些GAS优化技巧都是围绕着一些基础的思想和原则进行展开的,下面会简单介绍一下这些思想和原则。
区分交易 Gas 和 部署 Gas 交易Gas:用户每次与智能合约交互时支付的 Gas 量。实现 Gas 高效的函数,必须尽可能地减少Gas消耗。
部署Gas :每次部署智能合约时,需要支付的Gas量。部署智能合约通常只发生一次,尽管如此,仍然可以通过一些技巧来节省部署Gas 。
有时,减少一种Gas的技术会导致另一种Gas的增加,这是我们必须处理的权衡
下面列出在编写智能合约时需要关注的点,以便节省Gas。
- 尽量减少链上数据(使用事件、IPFS、无状态合约、merkle证明)。
- 最小化链上操作(字符串、返回存储值、循环、本地存储、批处理)
- 内存位置(calldata、栈、内存、存储)。
- 变量顺序
- 首选的数据类型
- 库(嵌入式库,独立部署库合约)。
- 最小代理(Minimal Proxy)
- 构造函数
- 合约大小(消息、修改器、函数)。
- Solidity编译器优化器
GAS面试题解析
1.在 EIP-1559 之前,如何计算以太坊交易的成本?
如前文介绍,在2021 年 8 月 5 日(伦敦升级)之前,以太坊的GAS费计算机制是简单粗暴的乘法和按价格排序机制。 用户设置手续费预算:指定 gas limit 和 gas price (发起者余额需要大于二者乘积) 竞拍方式:矿工把区块空间(资源)依次给最高的出价者(gas price) 矿工收益 = 用户手续费用 = gas 消耗(< gas limit) * gas price单价
2.伦敦升级后,每个区块的 gas 限制是多少?
伦敦升级(London Upgrade)是在 2021 年 8 月 5 日上线的,其中核心提案是 EIP-1559,它彻底改变了以太坊的 gas 费用机制,并引入了新的 区块 gas 限制逻辑。大概是上限30M。
2025年初社区推动上调至 36M、37M、目标 45M。
目前最新的区块数据,GASUsed:37,943,868(84.24%),Gas Limit: 45,043,944。
3.如何在 Solidity 中编写高效的 gas 循环?
- 避免无限循环。
- 避免重复计算和读取:多次计算相同的值,应该将该值存储在变量中,并在需要时重复使用该变量。
- 避免使用昂贵的操作:例如除法和取模。尝试使用更便宜的操作来代替它们。
- 避免使用大型数组:如果可能的话,应该使用映射或其他数据结构来代替数组。
- 避免使用复杂的嵌套循环
- 使用constant和immutable关键字:如果需要在循环中使用常量或不可变变量,请使用constant和immutable关键字来声明它们。
- 前置递增计数器,++i 比 i++ 更省 gas(无临时变量)
4.以太坊如何确定 EIP-1559 中的 BASEFEE?
Base Fee的设计初衷用一句话就可以描述,就是尽量让目标区块使用率设为 50%,让网络在“半满”状态下运行,避免极端拥堵。 所以影响Base fee的唯一因素就是上一个区块打包的整体 gas limit 是否使用超过了一半。如果超过了一半,就提升下一个区块的 Base Fee,最多提升 12.5%;如果没有超过一半,就减少下一个区块的 Base Fee,最多减少 12.5%。
5.payable 函数对 gas 的影响是什么?
在 Solidity 中,函数是否标记为 payable 本身不会显著影响 gas 开销,但它的存在有特定作用和潜在影响,payable 是 Solidity 中修饰函数或地址的一种方式,表示该函数可以接收以太币(Ether),否则调用时若附带 msg.value > 0,将 revert(交易回滚)。 调用一个 payable 函数,gas 分成:
- 基础 gas(21000):这是 EVM 执行任何交易的基础成本。
- 合约执行逻辑的 gas:如果函数内部执行了逻辑代码(比如转账、事件、计算),这部分才是主力开销。
- 写入 storage(如果有):最贵(2万 gas 起)
- 事件日志 emit:中等(375 gas + 每个字节数据) 所以,是否加 payable 修饰符,不是 gas 开销的决定因素,而是有没有做“和接收 ETH 相关的操作”。
6.什么是 gas griefing ?
Gas griefing 是一种合约攻击手段,攻击者通过构造特定的交易来消耗合约调用者的 gas,使其执行失败或消耗更多 gas。
常见于合约在回调外部地址(如使用 call 或 transfer)时,攻击者可以在回调函数(fallback 或 receive)中执行高 gas 消耗的操作,甚至导致调用失败(例如通过 revert 或无限循环)。这种攻击在支付合约中尤其危险,因为攻击者可以阻止其他正常用户接收付款。
为了防止gas griefing攻击,可以使用以下方法:
- 在智能合约中检查子调用所需的gas量,并确保为其提供足够的gas。
- 使用安全的编程实践来编写智能合约,例如避免使用循环和递归等高消耗操作。
- 使用Solidity的require和assert语句来确保智能合约的正确性和安全性。
7.描述三种存储 gas 成本类型
EVM 中存储操作的 gas 成本包括:
- SSTORE_INIT(20000 gas):把 storage 槽从 0 → 非0 设置。
- SSTORE_UPDATE(5000 gas):把 storage 从 非0 → 非0 修改(值变更)。
- SSTORE_CLEAR(5000 但退还 15000):把 storage 从 非0 → 0,部分 gas 退还。
8.乘以和除以二的倍数的 gas 高效替代方法是什么?
Solidity中,将一个数乘以或除以2的倍数可以通过移位运算来实现,这比使用乘法或除法运算更高效。
具体来说,将一个数左移n位相当于将它乘以2的n次方,而将一个数右移n位相当于将它除以2的n次方。
例如,将一个数除以8可以通过将它右移3位来实现,而将一个数乘以16可以通过将它左移4位来实现。在Solidity 0.8.3及更高版本中,编译器会自动将乘法和除法运算转换为移位运算,从而提高代码的效率。
乘以2的倍数:使用左移操作(x << n,相当于 x * 2**n)。
除以2的倍数:使用右移操作(x >> n,相当于 x / 2**n,向下取整)。
9.哪些操作会部分退还 gas?
SSTORE 操作:如果将一个存储槽从非零值更改为零值,则会退还一部分gas(最多退还 15000 gas)。
SLOAD 操作:如果从存储中读取一个非零值,则会退还一部分gas。
CALL 操作:如果调用的合约执行成功,则会退还一部分gas。
RETURN 操作:如果从函数中返回,则会退还一部分gas。
SELFDESTRUCT 操作:如果自毁合约,则会退还其余的gas。(退还 24000 gas)
需要注意的是,这些操作的退还gas的数量是固定的,具体取决于操作的类型和执行的环境。
注意:EIP-3529 限制了最大 gas refund 为总 gas 使用量的 20%,防止“gas token”滥用。并且每个区块的退还总量也有上限(目前为最大 gas 限制的50%)
REVERT 和 INVALID 不会退还 gas,但会停止执行并返还未用 gas。
10.函数名称如何影响 gas 成本,如果有的话?
函数名称本身不会影响运行时 gas 成本,但会影响部署成本(bytecode size):
函数名称越长,编译后的 metadata 和 debug 信息越大,增加部署 gas。
函数选择器(selector)碰撞风险:不是 gas 相关,但存在安全隐患。
总结:函数名长短几乎不影响调用成本,但会略微影响部署 gas。
11.为什么严格的不相等比较比 ≤ 或 ≥ 更节省 gas?额外的操作码是什么?
EVM 没有用于检查小于等于或大于等于的操作码。
操作码解析:
严格不等(!=):编译为 EQ + ISZERO(共 2 操作码)
小于 < 和 大于 >:编译为 LT 和 GT (共 1 操作码)
而 <= 和 >= 实际上是:
x <= y → !(x > y) → 使用 GT + ISZERO(共 2 操作码)
x >= y → !(x < y) → 使用 LT + ISZERO(共 2 操作码)
额外操作码是 ISZERO,增加 gas 消耗。
12.如何执行一个不需要用户支付 gas 的交易?
通过元交易(Meta-Transaction) 实现:
- 用户对交易签名(链下)。
- 中继者(Relayer)支付 gas,提交签名和交易到链上。
- 合约验证签名并执行逻辑(如使用 OpenZeppelin 的 MinimalForwarder)
13.在什么情况下,具有前导零的地址可以节省 gas,以及为什么?
当地址作为参数通过 calldata 传递时。 Calldata 中 零字节(0x00) 消耗 4 gas,非零字节消耗 16 gas。 前导零地址(如 0x0000...1234)包含更多零字节 → 传递成本更低。 例如:地址 0x0000...a1b2(16 个前导零):成本 = 16 * 4 + 4 * 16 = 128 gas。 随机地址(无前导零):成本 = 20 * 16 = 320 gas(节省 60%)。
14.在调用另一个智能合约时可以转发多少 gas?
通过 call 或 delegatecall 转发时,最多传递 当前可用 gas 的 63/64(约 98.4%)。这是 EIP-150 引入的防御机制,防止递归调用攻击。
使用 .gas(x) 可以显式设定转发 gas,但也要考虑安全问题。
15.为什么 calldata 中的负数会消耗更多的 gas?
负数在 calldata 中以 补码(Two's Complement)表示: 高位为符号位(负数时全 0xFF),导致大量非零字节。
非零字节消耗 16 gas(零字节仅 4 gas)。
例如 -1 编码为 0xffff...ffff(全是非零字节),比起一个小整数(例如 0x01,大量前导零)更昂贵