智能合約安全與審計

未檢查的外部呼叫

未檢查的外部呼叫是忽略低階呼叫的成功旗標,使一筆其實失敗了的轉帳或互動被默默當成成功。在 EVM 上,低階運算 call、delegatecall、staticcall 以及 send 方法在失敗時回傳的是一個布林值,而非回滾。若合約不檢視這個布林值,它便當作一切順利地繼續執行,帳目於是與現實脫節:一筆被標記為已送出的款項其實從未送達,或一個仰賴該呼叫效果的合約照樣往下走。

關鍵的區別在低階與高階呼叫之間。對具型別介面的高階 Solidity 呼叫,以及 address.transfer 方法,在失敗時會自動回滾整筆交易,因此錯誤無法被忽略。但 address.call{value: ...}("") 與 address.send 回傳的是 (bool success),失敗時仍繼續執行,把檢查留給開發者。經典的 King of the Ether 漏洞與許多資金損失事件,都源於漏掉了對那個回傳布林值的 require。

ERC-20 代幣讓這點更尖銳。標準說 transfer 與 transferFrom 回傳一個 bool,但一些廣泛使用的代幣(尤其是 USDT)什麼都不回傳,少數還會回滾而非回傳 false。假設回傳值一律是 bool 的程式碼,要麼在這些非標準代幣上回滾,要麼更糟地忽略結果。公認的修法是:永遠檢查低階呼叫回傳的成功與否,並使用像 OpenZeppelin 的 SafeERC20 這類包裝,其 safeTransfer 會把這些行為全部正規化,並在任何失敗時回滾。

// VULNERABLE: ignores success
msg.sender.call{value: amount}("");
balances[msg.sender] = 0;

// SAFE
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
balances[msg.sender] = 0;

低階呼叫的回傳值必須被檢查。

低階的 call 與 send 失敗時回傳 false 而非回滾;高階呼叫與 transfer 則回滾。把它們搞混、或忽略那個布林值,正是資金悄無聲息消失的途徑。ERC-20 讓情況更糟,所以 SafeERC20 包裝的存在是有道理的。

又称
unchecked return value未檢查回傳值