為什麼合約互動預演通過了、上鏈卻失敗

2026-09-03

為什麼合約互動預演通過了、上鏈卻失敗

錢包為這筆兌換做了預演,顯示你會收到多少,並報告沒有問題。你簽了名,交易卻以失敗的形態落在鏈上,而且照樣扣了 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(幣貝)學院的更多內容。

相關閱讀

幣貝上與本主題相關的其他文章:

- 如何安全地切換 RPC 端點

- 最高交易費超限:錢包這條警告到底在說甚麼

- RPC 速率限制超出:原因與應對

- 加密成本基礎的 FIFO 與 LIFO

- 迷因溢價:為什麼迷因會影響價格

風險披露:本文為 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

相關推薦

更多推薦