GnosisSafeProxy 學習
GnosisSafe是以太坊區塊鍊上最流行的多簽錢包!它的最初版本叫,現在新的錢包叫
MultiSigWallet
,意味着它不僅僅是錢包了。它自己的介紹為:以太坊上的最可信的數字資産管理平台(The most trusted platform to manage digital assets on Ethereum)。
Gnosis Safe
Gnosis Safe Contracts
的核心合約采用了代理/實作這種模式,并且為了友善大家建立,使用了ProxyFractory合約來進行代理合約的建立(當然建立代理合約之前必須建立實作合約)。
這裡什麼是代理/實作模式就不再講了,不清楚的讀者可以自行閱讀相關文章。
1.1 GnosisSafeProxy.sol 合約源碼
既然是代理/實作合約,那麼我們平常互動的對象就是代理合約了,雖然邏輯在實作合約裡面。相對其它而言,代理合約是非常簡單的,和
openzeppelin
的代理合約也很相似,我們先看本合約源碼。
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
/// @title IProxy - Helper interface to access masterCopy of the Proxy on-chain
/// @author Richard Meissner - <[email protected]>
interface IProxy {
function masterCopy() external view returns (address);
}
/// @title GnosisSafeProxy - Generic proxy contract allows to execute all transactions applying the code of a master contract.
/// @author Stefan George - <[email protected]>
/// @author Richard Meissner - <[email protected]>
contract GnosisSafeProxy {
// singleton always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated.
// To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt`
address internal singleton;
/// @dev Constructor function sets address of singleton contract.
/// @param _singleton Singleton address.
constructor(address _singleton) {
require(_singleton != address(0), "Invalid singleton address provided");
singleton = _singleton;
}
/// @dev Fallback function forwards all transactions and returns all received return data.
fallback() external payable {
// solhint-disable-next-line no-inline-assembly
assembly {
let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
mstore(0, _singleton)
return(0, 0x20)
}
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
}
1.2 源碼學習
注意:閱讀注釋很重要,魔鬼細節全在注釋裡。
我們現在開始學習,直接跳過版權聲明和
pragma
聲明部分。
-
定義了一個代理合約需要實作的接口,它僅有一個函數IProxy
,功能為傳回其實作合約位址。masterCopy()
-
代理合約定義。注意注釋中提到,它會根據contract GnosisSafeProxy
合約中的代碼來執行所有交易(其實這裡有一個例外,就是master
函數本身。注意,合約定義并沒有masterCopy
,也就是不需要顯式實作is IProxy
函數。這是因為為了節省masterCopy
,該函數統一通過gas
函數來實作,是以不需要顯式定義合約必須實作fallback
接口。IProxy
-
字面意思類似Java中單例,也就是唯一實作singleton
。注意,它是合約中的第一個狀态變量,是以存儲在插槽0。實作合約中的相同的狀态變量必須和代理合約中保持插槽順序一緻(否則會引起插槽沖突),也就是說實作合約的第一個狀态變量必須也是master
。這個我們以後學習到實作合約時再做驗證。singleton
- 注釋中提到它是内部可見性,是為了節省gas。它可以通過
也就是直接讀取插槽位置擷取,當然了,本合約中可以通過getStorageAt
定義的接口函數IProxy
擷取,當然,它内部也是通過讀取插槽0實作的。masterCopy
- 構造器參數是實作合約位址,驗證了它不能為0位址,這個很簡單,當然我們可以進一步驗證其它必須為合約位址。
-
函數。我們知道,調用一個合約時,如果合約比對不到相應的函數,則會調用fallback
函數(如果有定義)。代理/實作模式利用了這一特點,在fallback
函數裡将所有的調用轉為調用實作合約中相應的邏輯,再傳回相應結果。因為本合約未定義fallback
函數,是以接收ETH也是執行的本函數。receive
- 本列中的
函數和fallback
合約中的略有不同,首先,它判斷了調用是否為openzeppelin
函數,如果是的話,直接傳回masterCopy
位址,是以變相實作了singleton
。如果不是調用的IProxy
masterCopy
函數,則委托調用實作合約的相關邏輯。我們來簡單學習一下它的代碼。
需要注意的是,在内嵌彙編中,所有的
涉及的資料類型都是uint256類型,沒有其它類型。接下來的文檔中如果沒有特殊說明,所有的EVM dialect
均指32位元組(256位)。EVM中的操作一般是以一個word為機關的。word
-
這行代碼先讀取插槽0的資料(32位元組,256位),然後和40個let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
按位與操作,重置前面未使用的資料位為0。這是一個良好的習慣,我們不能假定前面未使用的資料位一定為0,雖然本例中的确為0。最後的結果得到F
位址,注意前面提到過,其不是位址類型,而是singleton
。uint256
-
判斷調用是否為if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) { mstore(0, _singleton) return(0, 0x20) }
。注意,雖然我們平常調用合約時,類似masterCopy
這樣的沒有參數的函數調用它的資料隻有8位masterCopy
(函數選擇器),但是0xa619486e
讀取的是calldata中的 位址開始的一個word内容,它是256位的,不足的話會被右邊補0。是以calldataload
語句中相比較的是補0後的函數選擇器,那麼補了多少個0呢?由于uint256是64個16進制長度,函數選擇器的長度是8,是以補了if
64 - 8 = 56
個0.
如果比較相等,則把
位址儲存到記憶體中0位址開始的位元組中去,然後傳回該位址。注意singleton
傳回記憶體中0位址開始的一個word,第一個參數0代表開始位址,第二個參數return(0, 0x20)
代表傳回内容的長度(位元組數)。0x20
也就是一個word(32位元組),剛好是上一步壓入記憶體的位址。0x20 = 32
- 如果不是
函數,則執行邏輯和masterCopy
中相關函數一緻,我們來看代碼:openzeppelin
第一行将所有的calldatacopy(0, 0, calldatasize()) let success := delegatecall(gas(), _singleton, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) if eq(success, 0) { revert(0, returndatasize()) } return(0, returndatasize())
calldata
資料複制到記憶體中(從calldata的0位址開始,複制到記憶體中的0位址開始位置)。
第二行進行委托調用,對應的參數按順序分别為剩餘的
,實作合約位址,記憶體中開始位址,資料大小,output開始位置 ,output大小(最後兩項一般為0)。 因為上一步複制了gas
到記憶體0位置,是以這裡我們是從0位址開始的,大小剛好就是calldata
calldatasize
。
第三行将傳回值複制到了記憶體中從0位址開始的位置(多次利用了零位址開頭的記憶體)。
4-6行判斷如果傳回值是0(代表delegatecall失敗),則将傳回值
revert
(這裡一般是出錯原因)。第一個參數0代表記憶體開始位置 ,第二個參數代表資料大小–位元組數。
第7行如果調用成功,則将傳回值
。(第一個參數0代表記憶體開始位置 ,第二個參數代表資料大小–位元組數)return
- 我們可以對比一下
中相關代碼openzeppelin
函數,基本是類似的:_delegate
Gnosis的代碼和這個相比,僅是多了一個function _delegate(address implementation) internal virtual { assembly { // Copy msg.data. We take full control of memory in this inline assembly // block because it will not return to Solidity code. We overwrite the // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) // Call the implementation. // out and outsize are 0 because we don't know the size yet. let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } }
的調用判斷及傳回。masterCopy
- 知識拓展。我們知道,在Solidity中,有自由記憶體指針,并且還有
。我們平常并不是從記憶體中零位址開始操作的,通常是從自由記憶體指針指向的位址開始操作的,一般為scratch
(前四個word已經被占用)。但是這裡0x80
的注釋解釋的很清楚,它并沒有采用Solidity的記憶體控制,而是自己完全控制,因為它不涉及到Solidity代碼(内嵌彙編是openzeppelin
代碼),是以是不沖突的。同時它還解釋了我們将Yul
最後兩個參數設定為0的原因是我們無法知道傳回值大小。delegatecall
就算學習結束了,它隻是一個簡單的代理合約。和标準的代理合約相比,它多了一個GnosisSafeProxy.sol
masterCopy
函數的調用判斷。
為什麼沒有把它單獨列為一個函數呢?根據注釋猜想應該是為了節省
gas
。
相對而言,
模闆中的openzeppelin
合約專門提供了一個函數TransparentUpgradeableProxy
用來傳回實作合約的位址。 另外,implementation
中的實作合約一般不是插槽位置0的狀态變量,例如實作了TransparentUpgradeableProxy
的eip-1967
合約,它的實作插槽是根據ERC1967Upgrade
"eip1967.proxy.implementation"
計算的哈希值減去1 得到的,雖然這樣會存在哈希碰撞的可能,但僅存于理論上。
采用相同插槽位置(從0開始)來儲存相同狀态變量的代理/實作模式還有CompoundV2版本的合約,大家有興趣的可以自己去看一下相關源碼。
拓展一點:
在它自己的通路提到了為什麼會有openzeppelin
.是因為本合約這種最簡單的代理實作模式可能存在函數選擇器沖突。如果實作合約恰好有一個函數的選擇器和TransparentUpgradeableProxy
相同(利用程式設計語言可以構造一個),那麼在調用這個函數時其實是會調用masterCopy
masterCopy
,進而得到的一個錯誤的結果。但是我們這裡的實作合約是固定的,是以不會存在這個問題。大家有興趣的可以參考:
https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy
-