从一次「失败但扣费」说起
很多新手第一次遇到「交易失败了,Gas 却烧了」会觉得自己被坑了。要理解这件事,得回到 EIP-140 出现之前:那时合约失败只有一种死法——执行到一半碰到无效指令或主动自毁,整个调用栈的状态全部回滚,但那笔交易承诺供应的 Gas 全部没收,一分不退。失败原因是什么?没有回答,只有一个回滚事实。
EIP-140 给 EVM 加了 REVERT 指令,随 2019 年 2 月的君士坦丁升级上主网。它改变了两个东西:第一,失败时只回滚状态,剩余的 Gas 退还给发送者;第二,回滚的同时可以返回一段任意字节——这正是今天钱包里那句「执行回滚:ERC20: transfer amount exceeds balance」的源头。

REVERT 和 out of gas 是两回事
同样是「没执行完」,协议里其实是两种命运。
REVERT 是主动认错:合约代码(或编译器生成的检查逻辑)明确判断「这个条件不满足」,于是执行 REVERT,状态回滚、 Gas 退剩余、错误数据带回。Solidity 里的 require 失败、函数 modifier 拦截、底层调用的 require(success),最后都落到这条指令上。
Out of gas 是燃尽了:Gas 用光时 EVM 抛出一个专门的异常,效果也是状态回滚,但性质完全不同——它不是合约「决定」失败,而是你给的 Gas 上限不够执行到终点。它不返回错误数据,收据状态码同样显示失败,排查时靠的是 Gas 曲线而不是消息。
对普通用户,两者的账单区别在于:REVERT 通常只烧掉实际执行的部分;Gas 没设够导致的失败则是按上限没收。这也是老手建议「估算值上浮一点再签」的原因——差得不多时两者花费接近,差得离谱时 out of gas 会多收一笔。
那段错误数据是怎么被读懂的
REVERT 返回的只是裸字节,能变成人话靠的是社区约定。Solidity 的 require 和 revert 语句把错误编码成:一个 4 字节的函数选择器(对错误签名做哈希取前四个字节),后面跟上 ABI 编码的参数。钱包或区块浏览器拿着这 4 个字节去已知错误表里比对,找到 Error(string) 或 Panic(uint256) 就把字符串或数字解码出来。查不到的字节就显示原始十六进制——这也是为什么有时你看到的失败提示是一串乱码般的 0x 开头:要么合约不是 Solidity 写的,要么这个项目的自定义错误还没进公开字典。
一个直觉算术
假设一笔 swap 要执行 8 万 Gas,Gas 上限设了 20 万。如果第 6 万步时滑点检查失败触发 REVERT:实际扣 8 万内的已用量,剩下约 12 万退回来,钱包显示失败原因。如果是 out of gas:按签名承诺的 20 万全额没收。同样失败的结局,账面差了一倍多,而失败发生在哪一步、用哪种方式失败,决定了你是「知情退款」还是「闷头扣费」。
收据状态码与事件日志的配合
区块浏览器判断「这笔交易到底成没成」,看的是收据里的状态字段:成功为一,失败为零。REVERT 只影响这个位和返回数据,不改收据结构。值得注意的组合是:合约可以先发出若干事件日志,再在某一步 REVERT——日志随状态一起作废,浏览器不会为它们建索引。所以「有日志等于成功」是错觉,检查成败永远以状态位为准。对做对账脚本的人,这条规则的实操含义是:扫描链上事件后,还应回查对应交易的状态位,否则会把一桩失败尝试计成真实业务。
快速问答
问:交易失败为什么还要矿工费? 答:节点和验证者已经为你的执行花了真实资源,协议按执行步数计费,不会因为你「没得到想要的结果」就免费。REVERT 能退的只是没执行那部分的预付额度。
问:我怎么提前避免失败扣费? 答:发送前用只读调用模拟一遍(例如 eth_call / estimateGas 这类不改变链状态的查询),条件不满足会在模拟里先暴露。模拟不等于保险:它反映的是当前链尖状态,价格、Nonce、池子余额在你确认前都可能变化,正式交易仍可能 REVERT。
风险提示
本文是协议机制科普,不构成投资建议。交易失败的处理方式因钱包和链而异,涉及资产操作请核对当期的错误含义与费用规则。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。