Solidity Data Collision
這是一篇關於 Proxy Contract 和 delegatecall 的注意事項。
Delegatecall
當 A 合約對 B 合約執行 delegatecall 時,B 合約的函式會被執行,但是對 storage 的操作都會作用在 A 合約上。舉例如下:
但是假如多加了一個 other
欄位在 _value
之前,執行合約之後反而是 other
欄位被更改了。
Storage Layout
了解上面的合約之前要先了解 Solidity 怎麼儲存 State Variables。Solidity Storage 以 Slot 為單位,每個 Slot 可以儲存 32 bytes 的資訊,一個 Contract 擁有 2**256 個 Slot。上述可以寫成一個映射關係 mapping(uint256 => bytes32) slots
。
Solidity 會從 Slot Index 為零開始分配給 State Variable。
除了 mapping
和 dynamically-sized array
,其他的 State Variable 會從 index 為零的 slot 開始被分配。
沒有宣告確切大小的 Array 會以 Slot Index 計算出一個雜湊值並將其作為 Slot Index。透過計算 keccak256(slot)
可以得知 _arr[0]
被存在哪裡,如果要取得 _arr[1]
則將計算出來的雜湊加上 Array 的 index 即可。
Mapping 則是以 Slot Index 和 Key 計算出一個雜湊值並將其作為 Slot Index。透過計算 keccak256(key, slot)
可以得到 mapping(key => value)
被存在哪。
Storage Collision
回到 DelegateExample_v2
的合約,對 B 來說, add
最後儲存加法的 Slot Index 為零,所以使用 A 的 Storage 執行 B 的函式結果自然會儲存在 A 的 other
裡,其 Slot Index 為 0。
這個問題也發生在 Proxy Contract,Layout 如下,當有需要更改 _owner
的操作,就會順帶把 _implementation
也更改了。
|Proxy |Implementation |
|--------------------------|-------------------------|
|address _implementation |address _owner | <= collision
|... |mapping _balances |
| |uint256 _supply |
| |... |
OpenZeppelin 處理的手法也很簡單,就是將 _implementation
換地方擺。以特定字串的雜湊值作為 Slot Index,儲存 Implementation 的地址。
|Proxy |Implementation |
|--------------------------|-------------------------|
|... |address _owner |
|... |mapping _balances |
|... |uint256 _supply |
|... |... |
|address _implementation | | <= specified
|... | |
hardhat-storage-layout
如何知道合約的 Storage Layout 呢?這邊推薦一個 Hardhat Plugin,按照文件就能得到合約的 Storage Layout。