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;
    }
}
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。