Solidity是以太坊生態系統中使用最廣泛的智能合約編程語言之一。然而,像任何軟件一樣,Solidity編譯器也存在一些缺陷和漏洞,這些問題可能會導致智能合約的不安全或不可靠性。本文將介紹一些solidity編譯器歷史上的一些高危漏洞

solidity特點

solidity是以太坊智能合約生態系統中最常用的編程語言之一。它允許開發人員編寫可執行的智能合約,并將這些合約部署到以太坊區塊鏈上。 Solidity編譯器通過將高級Solidity源代碼轉換為機器可讀的EVM字節碼來實現這一過程。

solidity是一種靜態類型的編程語言,用于開發在EVM上執行的智能合約。目前,使用EVM的區塊鏈平臺可以使用soldiity來開發智能合約,如以太坊、以太坊經典、幣安鏈、雪崩鏈、波場鏈等。

solidity有很多優點,第一是solidity在編譯時會執行靜態類型檢查,從而幫助開發人員避免一些常見的編程錯誤。第二,Solidity編譯器使用內存管理系統來分配和釋放內存,幫助開發人員避免一些內存管理錯誤。第三,Solidity編譯器提供了一套異常處理機制,幫助開發人員處理運行時錯誤。

歷史高危漏洞

有條件終止前的存儲寫入刪除

在編譯器版本0.8.13中,如果調用的函數中含有內聯匯編return()或stop()和條件判斷的函數可能會導致不正確的優化。

復現

contract C {
	uint public x;
	function f(bool a) public {
		x = 1; // This write is removed due to the bug.
		g(a);
		x = 2;
	}
	function g(bool a) internal {
		// The relevant part of this function is that it can
		// both return to the caller and terminate the transaction.
		// The bug will show its effects in the cases in which
		// the transaction is terminated (i.e. if a is false).
		// In this case the write x = 1 above will be missing.
		if (a) return;
		assembly { return(0,0) }
	}
}

首先使用solc-select選擇0.8.13版本的編譯器:

編譯合約,并開啟via-ir和optimize:

solc --bin --abi --via-ir --optimize c.sol

得到字節碼和abi:

將字節碼保存到bytecode.txt文件中,web3.py腳本將字節碼部署到本地測試網:

部署成功:

在remix中查看,首先x的初始值為0:

調用f并設a為false后,x為0:

再次調用f,這時設置a為true,這時候x為2:

這說明在運行return(0,0)的時候x=1被忽略了。

觸發條件

首先內聯匯編匯編中有return(0,0) 或者 stop()語句。其次是這個函數不應該有任何的變量讀寫即該函數是pure類型的。最后,函數中應該有個if控制流,其中一個是return(0,0) 或者 stop()語句,另一個是Storage的額外寫操作。

在打開Yul優化選項之后,優化器會執行Unused Store Eliminator,優化掉多余的寫操作:

{
    x = 1; // 多余語句,被優化
    x = 2;
}
{
    x = 1; // 由于函數最后會revert,所以也是多余語句,被優化
    revert();
}

但優化器此時將return(0,0) 和 stop()當作必然會revert處理了,當這兩個語句在if的一個分支中,而另一個分支正常時,優化器就會錯誤地將前面的寫操作優化。

關于內聯匯編的內存副作用的優化器錯誤

在編譯器版本0.8.13中有一個新的Yul優化步驟,用于刪除未使用的內存和存儲使用。

Yul優化器將最外層的Yul塊中從未讀取的所有內存寫入視為未使用,并將其刪除。當一個Yul塊是整個Yul程序的時候,這么做是對的。但如果一個程序分為多個塊時,優化器會單獨優化每個塊。這樣做的后果是如果一段內聯匯編匯編程序被分成多個部分,后面的代碼塊在訪問前面代碼塊中存儲在內存中的變量時會發生錯誤。

復現

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract C {
    function f() external pure returns (uint256 x) {
        assembly {
            mstore(0, 0x42)
        }
        assembly {
            x := mload(0)
        }
    }
}

使用0.8.13版本的solc編譯該合約,使用命令為:

solc --bin --abi --optimize 2022-4.sol

生成相應的abi和字節碼:

使用web3.py腳本部署該合約:

在remix中調用合約的f函數,可以發現返回結果為0,說明第一個內聯匯編塊中的mload語句被優化:

優化器Keccak緩存錯誤

在調用Keccak256內置函數的時候,如果被哈希計算的內容已知,字節碼優化器會進行特殊優化。但這里又個錯誤,如果被哈希的內容相同,但長度不同,這時優化器會錯誤地認為這兩個哈希值相同。

contract C {
  function bug() public returns (uint a, uint b) {
    assembly {
      mstore(0, 0)
      // The optimizer computes the value at compile time:
      // 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
      a := keccak256(0, 32)
      // The optimizer incorrectly uses the cached value
      // and transforms the next line to
      // b := 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
      // instead of 
      // b := 0xe2b9f9f9430b05bfa9a3abd3bac9a181434d23a707ef1cde8bd25d30203538d8
      b := keccak256(0, 23)
    }
  }
}

由于被哈希的內容已知,所以哈希計算會在編譯時完成。同時,兩次被哈希的內容相同,這時兩個哈希值a和b被優化器認為是相同的,盡管被哈希值實際長度不同。

contract C {
  function bug() public view returns (bool ret) {
    assembly {
      let x := calldataload(0)
      mstore(0, x)
      mstore(0x20, x)
      let a := keccak256(0, 4)
      // even though the memory location is different,
      // the 32-byte content is the same.
      let b := keccak256(0x20, 8)
      // Here `a` and `b` were considered equal,
      // leading to `ret` being incorrectly set to true.
      ret := eq(a, b)
    }
  }
}

在下面這個例子中,使用mstore將相同的值存放在下一個32bytes的內存中,但優化器依然認為它們是相等的。

究其原因,是在進行loadFromMemory操作時,是以32bytes為粒度進行,從而忽略了小于32bytes的情況。

復現

由于這個bug是0.8.13之前的所有版本都存在,所以這里使用0.8.0版本,開啟優化器編譯合約:

使用web3.py部署合約:

使用remix調用bug函數,返回結果為true:

空Byte Array復制錯誤

在solc版本小于0.7.4的編譯器中,如果創建了bytes或string類型的列表,復制空的bytes或string類型進入這個列表,再對這個列表的length或使用push()進行操作,會讓這個列表中的首個元素變成非0值。

contract C {
    bytes data;
    function f() public returns (bytes memory) {
        // Empty byte array
        bytes memory t;
        // Store something else in memory after it
        uint[2] memory x;
        x[0] = type(uint).max;
        // Copy the empty byte array to storage,
        // this will copy too much from memory.
        data = t;
        // Create a new byte array element,
        // this will only update the length value.
        data.push();
        // Now, `data[0]` is `0xff` instead of `0`.
        return data;
    }
}

復現

將測試代碼導入remix,選用編譯器版本0.7.0:

部署合約并調用f函數,可以看見返回的結果為非0值0xff: