Solidity 編譯器是否可以刪除外部呼叫?
我遇到了一種奇怪的情況,似乎 Solidity 編譯器(或優化器)正在刪除外部呼叫。
我正在使用 Hardhat 在分叉的主網上執行 ERC20 代幣轉移。在安全帽單元測試中,我檢查了在交易執行後令牌是否正確轉移。這是相關的程式碼片段:
// Solidity version 0.8.4 let SIG_TRANSFER := 0xa9059cbb00000000000000000000000000000000000000000000000000000000 let RECIPIENT_ADDR := 0xabc... let TOKEN_ADDR := 0xdef... let WITHDRAW_AMOUNT := 1000000000000000000 let free_mem_ptr := mload(0x40) mstore(free_mem_ptr, SIG_TRANSFER) // transfer(address,uint256) function signature mstore(add(free_mem_ptr, 0x4), RECIPIENT_ADDR) // recipient address mstore(add(free_mem_ptr, 0x24), WITHDRAW_AMOUNT) // amount to transfer let r := call(gas(), TOKEN_ADDR, 0x0, free_mem_ptr, 0x44, 0x0, 0x0) if eq(r, 0x0) { revert(0, 0) }
此程式碼按預期工作,當我執行它時,令牌會被轉移。未
revert()
執行,表示call
成功。然而有趣的是,如果我註釋掉revert
, 即... if eq(r, 0x0) { //revert(0, 0) } ...
交易仍然成功,但代幣餘額沒有改變。由於在第一次執行期間
revert
沒有被執行,因此call()
必須仍然成功,因為它沒有以任何方式更改。這裡發生了什麼?編譯器/優化器是否
call
因為我沒有使用返回值而刪除了我?同樣的事情也發生在pop
:pop(call(gas(), TOKEN_ADDR, 0x0, free_mem_ptr, 0x44, 0x0, 0x0))
沒有什麼是不可能的,但是如果
CALL
您的範例中的實際被刪除,那將是編譯器中的錯誤。我知道編譯器(我的意思是程式碼生成器)可能會跳過發出程式碼的唯一情況是,如果你有一個永遠不會被呼叫的內部函式。但是,刪除了整個功能,而不是單獨的指令。
至於優化器(可以修改已經發出的程式碼),它確實有一組內置註釋,其中一個是
canBeRemoved
但只有那些沒有副作用的才會得到這個註釋-CALL
絕對不是其中之一。您可以通過檢查
--asm
輸出來檢查指令是否實際被刪除(請參閱Analyzing the Compiler Output)。它是以人類可讀的彙編形式直接呈現字節碼,因此如果指令存在,它也必須存在於您編譯的合約中。revert() 未執行,表示呼叫成功。
不幸的是,這並不總是正確的。在進行低級呼叫時需要注意一個重要的問題(來自Units and Globally Available Variables > Members of Address Types):
由於 EVM 認為對不存在的合約的呼叫總是成功的,Solidity 包括
extcodesize
在執行外部呼叫時使用操作碼的額外檢查。這確保了即將被呼叫的合約要麼實際存在(它包含程式碼),要麼引發異常。
.call()
對地址而不是合約實例(即,.delegatecall()
,.staticcall()
和).send()
進行操作的低級呼叫.transfer()
不包括此檢查,這使得它們在 gas 方面更便宜,但也不太安全。您是否 100% 確定目標地址確實存在合約?您說令牌已轉移,所以我想確實如此,但可能值得仔細檢查。您可以添加
EXTCODESIZE
到您的程式碼以驗證這一點。