钱包给这笔兑换做了预演,显示你会收到多少,并报告没有问题。你签了名,交易却以失败的形态落在链上,而且照样扣了 gas。预演没有骗你。它回答的是关于某一个时刻的问题,而你的交易是在另一个时刻被执行的。
预演到底跑了什么
钱包里的预演是一次空跑。节点拿链上状态的一份副本执行你的调用,报告会发生什么,然后把结果扔掉。以太坊开发者文档把它背后的方法描述为“立即执行一次新的消息调用,而不在区块链上创建交易”,把配套的 gas 方法描述为“返回一个估算值,而该交易不会被加入区块链”。
从这段描述里能推出两条性质,两条在后面都有用。空跑是针对某一个选定的区块执行的,所以它的答案被钉死在那个区块的状态上。而且它是独自跑的,调用与结果之间没有别的东西在执行。
你预演时的状态,不是你落进去的状态
从预演到执行,你的交易要走一段路。它被签名、被广播、在内存池里等着、被出块者选中,然后才在包含它的那个区块的规则下执行。这趟路的每一步都要花时间,而链不会在这期间停下来。
合约代码读的是执行时刻的状态,从来不是预演时刻的状态。资金池储备、预言机的报数、一笔授权额度、一条白名单记录、一个暂停开关、一个单地址上限、一场已经结束的拍卖:这些值都可能在预演时是一个数、在出块时是另一个数。一个合约去检查这样的值、并在条件不满足时停下来,它在两个时刻的行为是完全一致的。变的是输入。
最小输出与截止时间:故意会失败的两道检查
一笔兑换调用可以把两道防护写进调用本身。一道是你必须收到的下限,由你的滑点容差推出来。另一道是一个时间戳,过了它这笔调用就不再有效。两者都是你签进去的参数,所以两者都被冻结在预演算出来的那个值上。
假设预演给你要卖的代币报出 10,000 USDC,而你的容差设成 0.5%。这笔调用于是带上了 9,950 USDC 的下限,合约收到的指令是:与其交付低于这个数的量,不如把整笔交互放弃掉。当交易还在路上、价格走得比你的容差更远时,这道防护做的正是你要求它做的事。截止时间的行为是一样的:一笔迟迟没确认、已经过了自己那个时间戳的调用,到达时就会被拒,哪怕同一笔调用早几分钟就能走通。
这正是“失败本身就是防护在起作用”的那一类。一道永远不会触发的防护,等于让这笔交互以出块那一刻市场漂到的任何价格成交。
排序:同一笔交易,换了个位置
一个区块是一个序列,而交易排序决定了哪一笔交互看到哪一份状态。你的预演把这笔调用放在一个空队列的最前面。区块则把它排在出块者放进去的其他东西后面,而这些邻居可以把你的调用正指望着的流动性、授权额度或剩余供应量消耗掉。
带硬上限的铸造把这件事摆得最直白。十个钱包可以各自把“最后一件”成功预演一遍,因为每一次预演跑的都是那件东西还没被领走的状态。其中一个先落地,另外九个撞上的是一个已经售罄的合约。那九次预演本身没有任何问题。它们回答的那个问题,在区块存在之前是一个答案,之后是另一个答案。
gas:估算不是预留
gas 估算的产生方式和预演一样,把调用执行一遍并测量它。同一份文档提醒,估算值“可能显著多于交易实际使用的 gas 量”,而真正扎人的是反方向:在一条便宜路径上测出来的估算,可能不够交易最终走的那条路径用。
执行分支就是这个缺口张开的地方。预演时经过一个池子的兑换,执行时可能要经过两个;对一个存储槽的首次写入,比后来再写一次更贵;一个循环预演时碰到三个仓位,执行时可能碰到九个。如果你签进去的上限在执行途中耗尽,做过的事会被撤销,而 gas 照样被消耗掉,这与任何一笔失败交易适用的是同一套算术。在估算之上留出余量并不会抬高手续费,因为gas 费的定价方式把工作量和单位工作量的价格分成了两件事。
当预演跑的根本不是你发出去的那笔
有时候预演和执行压根就不是同一次调用。预演是针对某一个网络上的某一个端点做的,所以一个指着另一条链、或者指着一个状态过期的节点的钱包,回答的是与你签名被广播进去的那个世界不同的另一个世界。
你自己那条待确认队列是这种错位的第二个来源。同一个账户的交易按 nonce 顺序执行,所以同一个钱包里更早的一笔待确认调用会先跑,并且能改掉后一笔调用所依赖的状态。当更早那笔正是一次还没确认的授权时,排在它后面的交互就会以你以为有的额度被预演、以你实际持有的额度被执行。
| 预演显示的 | 到执行时变掉的 | 该去看哪里 |
|---|---|---|
| 一个报出来的输出量 | 储备或预言机报数动了 | 签名调用里的最小输出参数 |
| 一笔有效的调用 | 签进去的时间戳过了 | 截止时间参数与待确认耗时 |
| 可用的供应量或流动性 | 同一区块里另一笔交易先拿走了 | 你这笔交易在区块里的位置 |
| 一个 gas 估算值 | 执行走了更长的分支 | 收据上的 gas 用量对 gas 上限 |
| 一次干净的空跑 | 钱包连在另一条网络或另一个节点上 | 签名时用的链标识与端点 |
失败之后长什么样
以这种方式停下来的交互是被记录下来的。它在一个区块里占着位置,它消耗了 gas,而它的收据带着一个状态字段:EIP-658 把收据里的中间状态根换成了一个状态码,其中零表示失败、一表示成功。区块浏览器把这个字段渲成已回退这个标签。
这个标签点的是结果,不是原因。有些合约在停下来时会附上一段原因字符串,浏览器或调用追踪能把它显示出来;另一些什么都不附。把失败交易和你真正签下去的那组参数放在一起读,才能把一个状态码变成一个诊断,因为那组参数正是收据没法替你还原的那一半。
什么才真的能降低失败率
把那段间隔缩短。在签名前一刻取的预演,描述的状态更接近区块最终会持有的那份;而一笔确认得快的交易,被别人抢在前面的时间窗更小。
按资产本身来设这两道防护,而不是按习惯设。一个紧到能把薄市场里一分钟的正常波动都拒掉的下限,会不断把你的交互停掉;一个松到什么都接受的下限,则放弃了你当初设它的那份保护。截止时间同理,它必须长到能熬过一段拥堵。
把反复失败当成信息。当同一笔交互带着同一组参数停了好几次,合约是在报告一个当前无法被满足的条件,而把同一笔调用再发一遍,是花 gas 去换同一个答案。
小结
预演回答的是:如果这笔调用现在、单独地、针对这个区块跑一遍,会发生什么。链上执行回答的是:这笔调用后来、在别的交易之间、针对另一个区块跑的时候,发生了什么。预演干净而上链失败,就是这两个问题之间的距离,而你签进去的那两道防护,正是把这段距离变成一次停下、而不是一次坏价成交的东西。去看签名调用里的参数、这笔交易在区块里的位置、以及收据上的 gas 用量对 gas 上限,原因就在这三者之一里。想继续学基础知识,请关注 Bitbase(币贝)学院的更多内容。
相关阅读
币贝上与本主题相关的其他文章:
风险披露:本文为 Bitbase(币贝)学院的科普内容,仅供教育与信息参考,不构成任何投资、交易、税务或财务建议。加密资产波动剧烈,请自行评估风险。本文撰写于 2026 年 9 月,请以官方最新信息为准。
参考资料
[1] Ethereum.org 开发者文档,JSON-RPC API(eth_call、eth_estimateGas) ethereum.org
[2] Ethereum Improvement Proposals,EIP-658: Embedding transaction status code in receipts eips.ethereum.org
[3] Ethereum.org 开发者文档,交易(Transactions) ethereum.org






