退款在回滚那一刻去哪了:EIP-3978 的失败补偿账 图 1
退款在回滚那一刻去哪了:EIP-3978 的失败补偿账 · 图 1

失败交易账单里最隐蔽的一格

以太坊的常识是”失败照样收 Gas”。但这句话里有一格很少有人拆开:失败路径上被抹掉的不只是退款资格,还有一本在失败前一路累加的账。现行 EVM 的规则是,某些释放状态的操作(典型如清零存储槽)执行时累积退款额度,交易结束统一结算;而任何调用帧一旦回滚,该帧名下已累积的退款记录直接清零。于是出现一组真实发生过、却不被任何规则承认的损失:合约先做了大量状态修改(付钱、挂退款),随后调用失败(退款作废),外层把交易做成——用户付出了真实的状态改动成本,退款资格却按失败一笔勾销。EIP-3978 要处理的正是这一格。

退款在回滚那一刻去哪了:EIP-3978 的失败补偿账 图 2
退款在回滚那一刻去哪了:EIP-3978 的失败补偿账 · 图 2

规范很薄:一本帧内的小账

提案的实现描述只有几行:给每个调用帧加一个 revert_gas_refund 计数器,初始为零;SSTORE、LOG0 至 LOG4、CALL、CREATE 与 CREATE2、SELFDESTRUCT 这几类会真正改动状态的操作执行时,按”这笔操作的实际 Gas 开销减去一次热存储读取的成本”累加进计数器;若帧回滚,交易层退款总额不是按旧规则抹除该帧旧账,而是把这本分账加回去。公式的气质很克制:补偿的是状态修改的开销,纯读取的成本维持现行处理不变。

为什么偏偏挑这几类操作

动机段的原则一句话可复述:Gas 应当反映真实使用。存储写入会弄脏数据库、日志要进区块的日志集合、建合约要跑初始化代码并更新账户序号——这些开销在失败路径上同样发生,旧规则把它们与”撤销状态”一视同仁地清零。而读取访问的成本模型(冷热之分)与失败无关,所以被公式减项扣掉,不进入补偿。这条边界也解释了为什么提案显得小:它不重新设计退款机制,只是把失败路径上”花了但没算”的科目登记回账本。

停滞的真实原因

正文的测试用例与安全考量两节长期停在占位符。全局记账规则的任何改动都要求全客户端等价实现与场景枚举:会不会给退款套利开出新通道、与退款上限规则怎么咬合、嵌套调用里分账簿的归属边界——这些答案缺一个,提案就进不了下一步。它因此是一个反面教材也是正面教材:机制描述再干净,缺少系统性的安全论证与测试资产,就停在原地。

与现行退款体系的相对位置

读这份提案前应先掌握现行主干:退款上限早已被大幅收紧,清库类激励弱化,失败交易照收执行费。EIP-3978 是在这套收紧后的体系内补失败细节,作者判断无已知向后不兼容。实用启发不在等它落地:智能合约的失败成本由动过多少状态决定,而不是由最后成没成功决定——明白这一点,才理解为什么钱包在模拟失败时坚持按真实 Gas 上限去试,也理解为什么”失败免费”从来不是链的真实行为。

一个算术例子:失败账单差在哪

设某内层调用依次做了三件事:把一个非零存储槽清为零(大额执行费、挂一笔额度可观的退款)、发一条日志、然后校验失败触发回滚。旧规则下结算账本的样子是:外层交易为这两步操作支付了执行 Gas,内层帧的退款记录在回滚瞬间整体清零,交易结束时可抵扣退款只剩外层其他帧的累积——失败帧的退款贡献为零。按 3978 的规则,同样的执行轨迹里,内层帧每做一步状态修改就往分账簿记一笔(操作费减去一次热读基础费的差额),回滚时这本分账转入交易退款池,失败路径的净成本相应下降。账本变化的方向值得看清:净支出下降的幅度与失败前的状态修改量成正比——失败前没动过状态的调用,两种规则算出同一张账单。这也是它作为提案的克制之处:只改差分,不碰成功路径,退款池的上限规则照旧封顶。

快速问答

问:被作废的退款算不算协议多收的钱? 答:机制上它是退款额度的作废而非新增收费,但经济效果确实是一笔真实开销没有折扣,这正是提案要抹平的缝。

问:我怎么估算一笔失败交易的代价? 答:失败交易的 Gas 用量如实记录执行消耗,乘当时 Gas 价即成本;跨帧退款结算以客户端行为为准。

风险提示:本文是 Gas 机制科普,不构成任何投资建议;计费规则以各 EIP 原文与客户端文档为准。