banner
zach

zach

github
twitter
medium

可升級合約解決方案

邏輯和存儲分離#

我們都知道,智能合約部署到區塊鏈上後合約的代碼是無法篡改的,一旦合約出現 bug 項目方很多時候也無計可施

合約通常由存儲的變量和邏輯函數組成,因為遷移變量到另一個新合約上開銷太大,並且主要升級的就是邏輯函數,因此可以將存儲的變量和邏輯函數進行合約層面上的隔離

因此完整的調用鏈路如下圖所示:

  • user 與 contract-A 交互,A 中不記錄函數邏輯,只存儲變量
  • contract-A 通過 delegatecall 調用 contract-B,將 B 中的函數邏輯作用於 contract-A
  • 調用結束,在 user 視角中只有 contract-A,不感知 contract-B 的存在

image

基礎的可升級合約實現#

參照上面的調用鏈路,如果將 contract-B 替換為其他合約,即可完成邏輯合約的更替而不影響原來的合約存儲

proxy 合約#

  • implementation:記錄了邏輯合約地址
  • 利用 fallback 函數,將函數調用全部通過 delegatecall 轉移到邏輯合約上
  • 提供 upgrade 函數用於更換邏輯合約地址
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// 簡單的可升級合約,管理員可以通過升級函數更改邏輯合約地址,從而改變合約的邏輯。
contract SimpleUpgrade {
    address public implementation; // 邏輯合約地址
    address public admin; // admin地址
    string public words; // 字符串,可以通過邏輯合約的函數改變
    // 構造函數,初始化admin和邏輯合約地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }
    // fallback函數,將調用委託給邏輯合約
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
    // 升級函數,改變邏輯合約地址,只能由admin調用
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

logic 合約#

  • 需要保持和 proxy 合約相同的存儲佈局(變量類型、變量順序)
  • 邏輯合約中的函數將會將變量的修改作用到 proxy 合約上,因此調用foo()函數後將會將 proxy 中的words變量修改為old
contract Logic1 {
    // 狀態變量和proxy合約一致,防止插槽衝突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通過邏輯合約的函數改變
    // 改變proxy中狀態變量,選擇器: 0xc2985578
    function foo() public{
        words = "old";
    }
}

新的邏輯合約:

  • 保持一致的存儲佈局
  • 調整函數邏輯,完成邏輯的更新,調用foo()函數後將會將 proxy 中的words變量修改為new
contract Logic2 {
    // 狀態變量和proxy合約一致,防止插槽衝突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通過邏輯合約的函數改變

    // 改變proxy中狀態變量,選擇器:0xc2985578
    function foo() public{
        words = "new";
    }
}

合約升級#

admin 賬戶調用 proxy 合約中的upgrade 方法,傳入新的邏輯合約地址,即可完成升級

存在的問題#

在 fallback 中調用 delegatecall 可能會出現 selector 衝突的問題

函數的選擇器 selector 就是函數簽名的哈希前四個字節,不同的函數是存在衝突的可能性的,如果合約中存在兩個選擇器衝突的函數時,合約是無法完成編譯的,但是 proxy 合約和邏輯合約之間可能存在衝突,在編譯環節無法檢測

透明代理#

透明代理就是為了解決選擇器衝突而導致的升級問題,因此透明代理的思路是將 upgrade 函數的權限和 fallback 中執行 delegatecall 的權限隔離開

  • fallback 函數,要求不是 admin 調用
  • upgrade 函數,要求必須是 admin 調用

以此避免因函數選擇器衝突導致的升級問題

fallback() external payable {
        require(msg.sender != admin);
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

  // 升級函數,改變邏輯合約地址,只能由admin調用
  function upgrade(address newImplementation) external {
      if (msg.sender != admin) revert();
      implementation = newImplementation;
  }

通用可升級代理 UUPS#

透明代理的模式每次都需要校驗管理員地址,耗費 gas

UUPS 模式將升級的函數放在邏輯合約裡,在單合約中可在編譯時就避免升級函數的選擇器衝突問題

原先的升級函數是在 proxy 合約中,且限制為 admin 權限,現在挪到 logic 合約中,由於 delegatecall 過程中 msg.sender 是不變的,而且存儲的 admin 依舊是 proxy 合約中記錄的,因此這個校驗邏輯放在 logic 合約中一樣可行

Untitled

UUPS 升級合約示例如下:在邏輯合約中記錄 implementation 地址,並作用到 proxy 合約中

proxy 合約#

只有存儲的變量、構造函數和 fallback,去掉了升級函數

contract UUPSProxy {
    address public implementation; // 邏輯合約地址
    address public admin; // admin地址
    string public words; // 字符串,可以通過邏輯合約的函數改變

    // 構造函數,初始化admin和邏輯合約地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback函數,將調用委託給邏輯合約
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
}

logic 合約#

增加了升級函數

// UUPS邏輯合約(升級函數寫在邏輯合約內)
contract UUPS1{
    // 狀態變量和proxy合約一致,防止插槽衝突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通過邏輯合約的函數改變

    // 改變proxy中狀態變量,選擇器: 0xc2985578
    function foo() public{
        words = "old";
    }

    // 升級函數,改變邏輯合約地址,只能由admin調用。選擇器:0x0900f010
    // UUPS中,邏輯函數中必須包含升級函數,不然就不能再升級了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
// 新的UUPS邏輯合約
contract UUPS2{
    // 狀態變量和proxy合約一致,防止插槽衝突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通過邏輯合約的函數改變

    // 改變proxy中狀態變量,選擇器: 0xc2985578
    function foo() public{
        words = "new";
    }

    // 升級函數,改變邏輯合約地址,只能由admin調用。選擇器:0x0900f010
    // UUPS中,邏輯函數中必須包含升級函數,不然就不能再升級了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。