天天看點

GnosisSafeProxy合約學習GnosisSafeProxy 學習

GnosisSafeProxy 學習

GnosisSafe是以太坊區塊鍊上最流行的多簽錢包!它的最初版本叫

MultiSigWallet

,現在新的錢包叫

Gnosis Safe

,意味着它不僅僅是錢包了。它自己的介紹為:以太坊上的最可信的數字資産管理平台(The most trusted platform to manage digital assets on Ethereum)。

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

    接口。
  • singleton

    字面意思類似Java中單例,也就是唯一實作

    master

    。注意,它是合約中的第一個狀态變量,是以存儲在插槽0。實作合約中的相同的狀态變量必須和代理合約中保持插槽順序一緻(否則會引起插槽沖突),也就是說實作合約的第一個狀态變量必須也是

    singleton

    。這個我們以後學習到實作合約時再做驗證。
  • 注釋中提到它是内部可見性,是為了節省gas。它可以通過

    getStorageAt

    也就是直接讀取插槽位置擷取,當然了,本合約中可以通過

    IProxy

    定義的接口函數

    masterCopy

    擷取,當然,它内部也是通過讀取插槽0實作的。
  • 構造器參數是實作合約位址,驗證了它不能為0位址,這個很簡單,當然我們可以進一步驗證其它必須為合約位址。
  • fallback

    函數。我們知道,調用一個合約時,如果合約比對不到相應的函數,則會調用

    fallback

    函數(如果有定義)。代理/實作模式利用了這一特點,在

    fallback

    函數裡将所有的調用轉為調用實作合約中相應的邏輯,再傳回相應結果。因為本合約未定義

    receive

    函數,是以接收ETH也是執行的本函數。
  • 本列中的

    fallback

    函數和

    openzeppelin

    合約中的略有不同,首先,它判斷了調用是否為

    masterCopy

    函數,如果是的話,直接傳回

    singleton

    位址,是以變相實作了

    IProxy

    。如果不是調用的

    masterCopy

    函數,則委托調用實作合約的相關邏輯。我們來簡單學習一下它的代碼。

    需要注意的是,在内嵌彙編中,所有的

    EVM dialect

    涉及的資料類型都是uint256類型,沒有其它類型。接下來的文檔中如果沒有特殊說明,所有的

    word

    均指32位元組(256位)。EVM中的操作一般是以一個word為機關的。
    1. let _singleton := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)

      這行代碼先讀取插槽0的資料(32位元組,256位),然後和40個

      F

      按位與操作,重置前面未使用的資料位為0。這是一個良好的習慣,我們不能假定前面未使用的資料位一定為0,雖然本例中的确為0。最後的結果得到

      singleton

      位址,注意前面提到過,其不是位址類型,而是

      uint256

    2. if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000) {
          mstore(0, _singleton)
          return(0, 0x20)
      }
                 
      判斷調用是否為

      masterCopy

      。注意,雖然我們平常調用合約時,類似

      masterCopy

      這樣的沒有參數的函數調用它的資料隻有8位

      0xa619486e

      (函數選擇器),但是

      calldataload

      讀取的是calldata中的 位址開始的一個word内容,它是256位的,不足的話會被右邊補0。是以

      if

      語句中相比較的是補0後的函數選擇器,那麼補了多少個0呢?由于uint256是64個16進制長度,函數選擇器的長度是8,是以補了

      64 - 8 = 56

      個0.

      如果比較相等,則把

      singleton

      位址儲存到記憶體中0位址開始的位元組中去,然後傳回該位址。注意

      return(0, 0x20)

      傳回記憶體中0位址開始的一個word,第一個參數0代表開始位址,第二個參數

      0x20

      代表傳回内容的長度(位元組數)。

      0x20 = 32

      也就是一個word(32位元組),剛好是上一步壓入記憶體的位址。
    3. 如果不是

      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位址開始位置)。

      第二行進行委托調用,對應的參數按順序分别為剩餘的

      gas

      ,實作合約位址,記憶體中開始位址,資料大小,output開始位置 ,output大小(最後兩項一般為0)。 因為上一步複制了

      calldata

      到記憶體0位置,是以這裡我們是從0位址開始的,大小剛好就是

      calldatasize

      第三行将傳回值複制到了記憶體中從0位址開始的位置(多次利用了零位址開頭的記憶體)。

      4-6行判斷如果傳回值是0(代表delegatecall失敗),則将傳回值

      revert

      (這裡一般是出錯原因)。第一個參數0代表記憶體開始位置 ,第二個參數代表資料大小–位元組數。

      第7行如果調用成功,則将傳回值

      return

      。(第一個參數0代表記憶體開始位置 ,第二個參數代表資料大小–位元組數)
    4. 我們可以對比一下

      openzeppelin

      中相關代碼

      _delegate

      函數,基本是類似的:
      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())
              }
          }
      }
                 
      Gnosis的代碼和這個相比,僅是多了一個

      masterCopy

      的調用判斷及傳回。
    5. 知識拓展。我們知道,在Solidity中,有自由記憶體指針,并且還有

      scratch

      。我們平常并不是從記憶體中零位址開始操作的,通常是從自由記憶體指針指向的位址開始操作的,一般為

      0x80

      (前四個word已經被占用)。但是這裡

      openzeppelin

      的注釋解釋的很清楚,它并沒有采用Solidity的記憶體控制,而是自己完全控制,因為它不涉及到Solidity代碼(内嵌彙編是

      Yul

      代碼),是以是不沖突的。同時它還解釋了我們将

      delegatecall

      最後兩個參數設定為0的原因是我們無法知道傳回值大小。
    好了,

    GnosisSafeProxy.sol

    就算學習結束了,它隻是一個簡單的代理合約。和标準的代理合約相比,它多了一個

    masterCopy

    函數的調用判斷。

    為什麼沒有把它單獨列為一個函數呢?根據注釋猜想應該是為了節省

    gas

    相對而言,

    openzeppelin

    模闆中的

    TransparentUpgradeableProxy

    合約專門提供了一個函數

    implementation

    用來傳回實作合約的位址。 另外,

    TransparentUpgradeableProxy

    中的實作合約一般不是插槽位置0的狀态變量,例如實作了

    eip-1967

    ERC1967Upgrade

    合約,它的實作插槽是根據

    "eip1967.proxy.implementation"

    計算的哈希值減去1 得到的,雖然這樣會存在哈希碰撞的可能,但僅存于理論上。

    采用相同插槽位置(從0開始)來儲存相同狀态變量的代理/實作模式還有CompoundV2版本的合約,大家有興趣的可以自己去看一下相關源碼。

    拓展一點:

    openzeppelin

    在它自己的通路提到了為什麼會有

    TransparentUpgradeableProxy

    .是因為本合約這種最簡單的代理實作模式可能存在函數選擇器沖突。如果實作合約恰好有一個函數的選擇器和

    masterCopy

    相同(利用程式設計語言可以構造一個),那麼在調用這個函數時其實是會調用

    masterCopy

    ,進而得到的一個錯誤的結果。但是我們這裡的實作合約是固定的,是以不會存在這個問題。大家有興趣的可以參考:

    https://docs.openzeppelin.com/contracts/4.x/api/proxy#TransparentUpgradeableProxy