智能合约部署后就不能变更 (设计上的不变性)。另一方面,软件质量在很大程度上取决于迭代升级和修补源代码的能力。尽管基于区块链的软件从不变性中获得了可观的收益,但仍需要一定程度的可变性才能修复错误和改进产品。

在这篇文章中,我们将学习:

  1. 为什么我们需要升级智能合约?2. 了解升级是如何进行的?3. 使用 OpenZeppelin CLI[1] 轻松编写 / 管理“可升级”智能合约。4. 使用 OpenZeppelin[2] 升级库以编程方式升级合约。5. 可升级合约的一些局限性和解决方法

如果您只是在寻找一种写可升级合约的方式,并且不想经历“这一切的工作原理”,那么请直接转到第三部分 : OpenZeppelin Upgrades[3]。

为什么我们需要升级智能合约

默认情况下,以太坊中的智能合约是不可变的。一旦创建了它们,就无法对其进行更改,从而有效地充当了参与者之间牢不可破的合约(Tiny 熊注:指因为不变性提供了参与者的信任)。

但是,在几种情况下,我们希望有一种升级合同的方法。有很多例子,其中价值数百万美元的以太币被盗 / 被黑客入侵 [4],如果我们可以更新智能合约,则可能可以阻止这些损失。

升级是如何进行的

我们可以通过几种方式升级合约。

最明显的方式将是这样的:

•创建并部署新版本的合约。•手动将所有状态从旧合约迁移到新合同。

这似乎可行,但是有几个问题。

  1. 迁移合约状态可能代价非常大。2. 当我们创建和部署新合约时,合约地址将更改。因此,我们需要更新与旧合约交互的所有合约,以使用新版本的地址。3. 您还必须与所有用户联系,并说服他们开始使用新合同并处理同时使用的两个合约,因为用户迁移速度很慢。

更好的方法是使用带有接口 [5] 的代理合约,其中每个方法都将委托给实现合约(包含所有逻辑)。

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

委托调用 deletage call [6] 与常规调用类似,不同之处在于,所有代码均在调用方(代理)的上下文中执行,而不在被调用方(实现)的上下文中执行。因此,实现合约代码中的 tranfer 将转移代理的余额,对合约存储的任何读取或写入都将从代理的存储中进行读取或写入。

这种方法更好,因为用户仅与代理合约进行交互,并且可以在保持代理合约不变的同时升级实现合约。

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

这似乎比以前的方法要好,但是如果需要对实现合约方法进行任何更改(Tiny 熊注:这里指变更方法名称),那么我们也需要更新代理合约的方法(因为代理合约具有接口方法)。因此,用户同样需要更改代理合约地址。

要解决此问题,我们可以在代理合约中使用 fallback 回退函数 [7]。 fallback 函数 [8] 将执行任何请求,将请求重定向到实现合约并返回结果值(使用操作码 [9])。这与以前的方法类似,但是这里的代理合约没有接口方法,只有 fallback 回退函数,因此,如果更改合约方法,则无需更改代理地址。

这是一个基本的解释,足以让我们处理可升级的合约。如果想深入研究代理合约代码和不同的代理模式,可以查看这篇文章 [10]。(这篇文章作者其实还没写 :))

OpenZeppelin Upgrades

正如我们在上面看到的,在编写可升级合约时,需要管理很多事情。

幸运的是,像 OpenZeppelin[11] 这样的项目已经构建了 CLI 工具和库 [12],它们为可任何治理结构控制的智能合约提供易于使用,简单,健壮和选择加入的升级机制,无论它是多签名钱包, 一个简单的地址或一个复杂的 DAO。

首先,我们使用 OpenZeppelin CLI 工具构建一个基本的可升级合约。您可以在此处 [13] 找到以下实现的代码。

OpenZeppelin Upgrades CLI

使用 OpenZeppelin CLI 需要使用 Node.js 进行开发。如果尚未安装,请使用您喜欢的包管理器或官方安装程序来安装 Node。

创建项目

创建一个名为 upgradable-smart-contracts 的文件夹,然后进入该文件夹。

    $ mkdir upgradable-smart-contracts && cd upgradable-smart-contracts

在本教程中,我们使用 $ 字符来指示终端的 shell 程序提示符。继续操作时,请勿输入 $ 字符,否则会出现一些奇怪的错误。

我们将在本教程中使用本地区块链网络。最受欢迎的本地区块链是 Ganache。运行以下命令启动一个本地网络:

    $ npm install --save-dev ganache-cli && npx ganache-cli --deterministic

现在,在同一文件夹中启动新的 shell 终端,运行以下命令来安装 CLI 工具:

    $ npm install --save-dev @openzeppelin/cli

要管理部署的合同,您需要创建一个新的 CLI 项目。运行以下命令,并在出现提示时为其提供名称和版本号:

    $ npx openzeppelin init

初始化期间将发生两件事。首先,将创建一个 .openzeppelin 目录,其中包含项目相关的信息。此目录将由 CLI 管理:您无需手动进行任何编辑。但是,应该将其中一些文件 [14] 提交给 Git。

其次,CLI 将网络配置存储在名为 networks.js 的文件中。为了方便起见,它已经填充了一个名为 development 的条目,其配置与 Ganache[15] 的默认值匹配。

可以通过运行以下命令来查看所有未锁定的帐户:

    $ npx openzeppelin accounts

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

编写及部署合约

现在,让我们在 contracts 文件夹中创建一个名为 TodoList 的合同。

    // contracts/TodoList.solpragma solidity ^0.6.3;  
    contract TodoList {    string[] private list;  
        // Emitted when the storeda new item is added to the list    event ItemAdded(string item);  
        // Adds a new item in the list    function addItem(string memory newItem) public {        list.push(newItem);        emit ItemAdded(newItem);    }  
        // Gets the item from the list according to index    function getListItem(uint256 index)        public        view        returns (string memory item)    {        return list[index];    }}

现在,让我们将该合同部署在本地区块链上。

    $ npx openzeppelin create

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

如我们所见,我们的合同已部署到 0xD833215cBcc3f914bD1C9ece3EE7BF8B14f841bb

让我们通过运行 npx openzeppelin send-tx,使用 addItem() 函数添加 item (参数:"responding to emails" )到数组。

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

现在,假设我们需要添加一个名为 getListSize() 的新函数来获取列表的大小。只需在 TodoList 合同中添加一个新函数即可。

    // contracts/TodoList.solpragma solidity ^0.6.3;  
    contract TodoList {    // ...  

        // Gets the size of the list    function getListSize() public view returns (uint256 size) {        return list.length;    }}

更改 Solidity 文件后,我们现在可以通过运行 openzeppelin upgrade 命令来升级之前部署的实例。

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

这样就可以了!我们的 TodoList 实例已升级到最新版本的代码,同时保持其状态和与以前相同的地址。我们不需要创建和部署代理合同或将代理链接到 TodoList。所有这些都是在后台进行的!

我们可以尝试调用新合同中的 getListSize() 函数并检查列表的大小:

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

而已!请注意,在整个升级过程中如何保留列表的大小及其地址。无论您使用的是本地区块链,测试网还是主网络,此过程都是相同的。

以编程方式升级合约

如果要通过 JavaScript 代码而不是通过命令行创建和升级合同,则可以使用 OpenZeppelin Upgrades 升级库 [16] 而不是 CLI。

CLI 不仅管理合同升级,而且还管理编译,交互和源代码验证。升级库仅负责创建和升级。该库也不会跟踪已经部署的合同,也不会像 CLI 那样运行任何初始化程序或验证存储空间。但是,这些功能可能会在不久的将来添加到 Upgrades 库中。

您可以在此处 [17] 找到以下实现的代码。

如果您没有遵循上述 OpenZeppelin CLI 部分,则需要按照此处的说明安装 NodeJs 和 Ganache[18]。

第一步是在您的项目中安装该库,您可能还希望安装 web3 以使用 JavaScript 与合同进行交互,并使用 @ openzeppelin / contract-loader 从 JSON 工件加载合同。

    $ npm install web3 @openzeppelin/upgrades @openzeppelin/contract-loader

现在,在 upgradable-smart-contracts 文件夹中创建一个文件 index.js,然后粘贴此样板代码。

    // index.jsconst Web3 = require("web3");const {  ZWeb3,  Contracts,  ProxyAdminProject} = require("@openzeppelin/upgrades");  
    async function main() {  // Set up web3 object, connected to the local development network, initialize the Upgrades library  const web3 = new Web3("http://localhost:8545");  ZWeb3.initialize(web3.currentProvider);  const loader = setupLoader({ provider: web3 }).web3;}  
    main();

在这里,我们设置了连接到本地开发网络的 web3 对象,通过 ZWeb3.initialize 初始化 Upgrades 库,并初始化合约 loader 加载器

现在,在 main() 中添加以下代码以创建一个新项目,以管理我们的可升级合同。

    async function main() {  // ...  
      //Fetch the default account  const from = await ZWeb3.defaultAccount();  
      //creating a new project, to manage our upgradeable contracts.  const project = new ProxyAdminProject("MyProject", null, null, {    from,    gas: 1e6,    gasPrice: 1e9  });}

现在,使用这个 project 我们可以创建任何合同的实例。该 project 将负责以后以可以升级的方式进行部署合约。

让我们在 upgradable-smart-contracts/contracts 文件夹中创建 2 个合约 TodoList1 及其更新的版本 TodoList2

    // contracts/TodoList1.solpragma solidity ^0.6.3;  
    contract TodoList1 {    string[] private list;  
        // Emitted when the storeda new item is added to the list    event ItemAdded(string item);  
        // Adds a new item in the list    function addItem(string memory newItem) public {        list.push(newItem);        emit ItemAdded(newItem);    }  
        // Gets the item from the list according to index    function getListItem(uint256 index)        public        view        returns (string memory item)    {        return list[index];    }}

要创建 TodoList2,只需在上述合约中添加一个新的 getListSize() 函数。

    // contracts/TodoList2.solpragma solidity ^0.6.3;  
    contract TodoList2 {    string[] private list;  
        // Emitted when the storeda new item is added to the list    event ItemAdded(string item);  
        // Adds a new item in the list    function addItem(string memory newItem) public {        list.push(newItem);        emit ItemAdded(newItem);    }  
        // Gets the item from the list according to index    function getListItem(uint256 index)        public        view        returns (string memory item)    {        return list[index];    }  
        // Gets the size of the list    function getListSize() public view returns (uint256 size) {        return list.length;    }}

现在,我们需要使用以下命令编译这两个合同:

    $ npx openzeppelin compile

这将在 build / contracts 文件夹中创建 JSON 合同 artifacts 工件。这些工件文件包含有关我们需要部署并与合同进行交互的所有信息。

现在,让我们使用上面创建的 project 创建 TodoList1 的实例。

    async function main() {//...  

    //Using this project, we can now create an instance of any contract.  //The project will take care of deploying it in such a way it can be upgraded later.  const TodoList1 = Contracts.getFromLocal("TodoList1");  const instance = await project.createProxy(TodoList1);  const address = instance.options.address;  console.log("Proxy Contract Address 1: ", address);}

在这里,我们从上面使用 Contracts.getFromLocal 创建的合同工件中获得 TodoList1 合同详细信息。然后,我们创建并部署一对代理和实现(TodoList1)合同,并通过 project.createProxy 方法将代理合同链接到 TodoList1。最后,我们打印出代理合同的地址。

现在,让我们使用 addItem() 方法将 item 添加到 list 中,然后使用 getListItem() 获取添加的项目。

    async function main() {//...  
      // Send a transaction to add a new item in the TodoList1  await todoList1.methods    .addItem("go to class")    .send({ from: from, gas: 100000, gasPrice: 1e6 });  
      // Call the getListItem() function to fetch the added item from TodoList1  var item = await todoList1.methods.getListItem(0).call();  console.log("TodoList1: List Item 0: ", item);}

现在,让我们将 TodoList1 合同升级为 TodoList2

    async function main() {//...  

    //After deploying the contract, you can upgrade it to a new version of  //the code using the upgradeProxy method, and providing the instance address.  const TodoList2 = Contracts.getFromLocal("TodoList2");  const updatedInstance = await project.upgradeProxy(address, TodoList2);  console.log("Proxy Contract Address 2: ", updatedInstance.options.address);}

在这里,我们从合同工件中获取 TodoList2 合同详细信息。然后,我们通过 project.upgradeProxy 方法更新合同,该方法带有 2 个参数,即上一步中部署的代理合同的地址和 TodoList2 合同对象。然后,我们在更新后打印出代理合同的地址。

现在,让我们向 TodoList2 添加一个新 item 并获取这些 item。

    async function main() {//...  

      // Send a transaction to add a new item in the TodoList2  await todoList2.methods    .addItem("code")    .send({ from: from, gas: 100000, gasPrice: 1e6 });  
      // Call the getListItem() function to fetch the added items from TodoList2  var item0 = await todoList2.methods.getListItem(0).call();  var item1 = await todoList2.methods.getListItem(1).call();  console.log("TodoList2: List Item 0: ", item0);  console.log("TodoList2: List Item 1: ", item1);}

现在,让我们使用 node index.js 运行 index.js

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

在这里我们可以观察到两件事:

•即使将 TodoList1 更新为 TodoList2,代理合同的地址也没有改变。•当我们从 TodoList2 中获得 2 个项目时,这表明状态在整个更新过程中都得到保留。

因此,可以说 TodoList1 实例已升级到代码的最新版本(TodoList2),同时保持其状态和与以前相同的地址

现在,正如我们已经看到了如何升级合同一样,让我们看看编写更复杂的合同时需要了解的一些限制和解决方法。

可升级合约的一些局限性和解决方法

使用 OpenZeppelin Upgrades 处理可升级合同时,在编写 Solidity 代码时要牢记一些小警告。

值得一提的是,这些限制源于 EVM 的工作原理 [19],并且不仅适用于 OpenZeppelin Upgrades ,而且适用于所有可升级合同的项目。

如果我们的合同不兼容升级,则当尝试升级时,CLI 会警告:OpenZeppelin 升级库中尚未提供此功能。

为了了解限制和变通办法,让我们以 Example 合同为例,探索合同中的限制并添加一些变通办法以使合同可升级。

    // contracts/Example.sol  
    pragma solidity ^0.6.0;  
    import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";  
    contract Example {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        constructor(uint8 cap) public {       _cap = cap;        token = new ERC20Capped(_cap);    }}

限制 1:没有构造函数

由于基于代理的可升级性的要求,因此在可升级合同中不能使用构造函数。要了解此限制的原因,请转至此文章 [20]。

解决方法:初始化

一种解决方法是用一个通常称为 initialize 的函数替换构造函数,在该函数中运行构造函数逻辑。

    // contracts/Example.solpragma solidity ^0.6.0;  
    import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";  
    contract Example {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        function initialize(uint8 cap) public {       _cap = cap;        token = new ERC20Capped(_cap);    }}

现在,由于初始化合同时构造函数 constructor 仅被调用一次,因此我们需要添加一个检查以确保初始化函数 initialize 仅被调用一次。

    // contracts/Example.solpragma solidity ^0.6.0;  
    import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";  
    contract Example {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        bool private_initialized = false;  
        function initialize(uint8 cap) public {        require(!_initialized);       _initialized = true;       _cap = cap;        token = new ERC20Capped(_cap);    }}

由于在编写可升级合同时这是很常见的事情,因此 OpenZeppelin Upgrades 提供了 Initializable 基类合同,该合同具有一个 initializer 修饰符,该修饰符可以解决以下问题:

    // contracts/Example.solpragma solidity ^0.6.0;  
    import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";import "@openzeppelin/upgrades/contracts/Initializable.sol";  
    contract Example is Initializable {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        function initialize(uint8 cap) public initializer {       _cap = cap;        token = new ERC20Capped(_cap);    }}

构造函数和常规函数之间的另一个区别是,Solidity 负责自动调用合同所有祖先的构造函数。在编写初始化程序时,您需要特别注意手动调用所有父合约的初始化函数 initializers:

    // contracts/Example.solpragma solidity ^0.6.0;  
    import "github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20Capped.sol";import "@openzeppelin/upgrades/contracts/Initializable.sol";  

    contract BaseExample is Initializable {    uint256 public createdAt;  
        function initialize() initializer public {        createdAt = block.timestamp;    }  
    }  
    contract Example is BaseExample {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        function initialize(uint8 cap) initializer public {       _cap = cap;        token = new ERC20Capped(_cap);    }}

请记住,此限制不仅会影响合同,还会影响您从库中导入的合同。例如,考虑来自 OpenZeppelin 合同中的 ERC20Capped:该合同在其构造函数中初始化 Token 的上限。

    pragma solidity ^0.6.0;  
    import "./ERC20.sol";  
    /*** @dev Extension of {ERC20} that adds a cap to the supply of tokens. */contract ERC20Capped is ERC20 {    uint256 private_cap;  
        /**    * @dev Sets the value of the `cap`. This value is immutable, it can only be     * set once during construction.     */    constructor (uint256 cap) public {        require(cap > 0, "ERC20Capped: cap is 0");       _cap = cap;    }  
        //...}

这意味着您不应在 OpenZeppelin Upgrades 项目中使用这些合同。相反,请确保使用 @openzeppelin/contracts-ethereum-package,这是 OpenZeppelin Contracts 的官方分支,已被修改为使用 initializers 而不是构造函数。看一下 @openzeppelin/contracts-ethereum-package 中 ERC20Capped[21] 长什么样:

    pragma solidity ^0.5.0;  
    import "@openzeppelin/upgrades/contracts/Initializable.sol";import "./ERC20Mintable.sol";  
    /*** @dev Extension of {ERC20Mintable} that adds a cap to the supply of tokens. */contract ERC20Capped is Initializable, ERC20Mintable {    uint256 private_cap;  
        /**    * @dev Sets the value of the `cap`. This value is immutable, it can only be     * set once during construction.     */    function initialize(uint256 cap, address sender) public initializer {        ERC20Mintable.initialize(sender);  
            require(cap > 0, "ERC20Capped: cap is 0");       _cap = cap;    }  
        //...}

无论使用 OpenZeppelin 合同还是其他以太坊软件包,请始终确保它们都已设置为处理可升级合同。

    // contracts/Example.solpragma solidity ^0.6.0;  
    import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20Capped.sol";import "@openzeppelin/upgrades/contracts/Initializable.sol";  

    contract BaseExample is Initializable {    uint256 public createdAt;  
        function initialize() initializer public {        createdAt = block.timestamp;    }  
    }  
    contract Example is BaseExample {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        function initialize(uint8 cap) initializer public {       _cap = cap;        token = new ERC20Capped(_cap);    }}

限制 2:状态变量声明中的初始值

Solidity 允许在合同中状态变量声明时定义初始值。

    contract Example is BaseExample {    uint256 private_cap = 1000000000000000000;  
        //...}

这等效于在构造函数中设置这些值,因此,不适用于可升级合同。

解决方法:初始化

确保在初始化函数中设置所有初始值,如下所示;否则,任何可升级实例都不会设置这些状态变量。

    //...  
    contract Example is BaseExample {    uint256 private_cap;    ERC20Capped public token;  
        function initialize(uint8 cap) initializer public {       _cap = 1000000000000000000;       _cap = cap;        token = new ERC20Capped(_cap);    }}

请注意,设置常量还是可以的,因为编译器不会为这些变量保留存储槽 [22],并且每次出现都将被相应的常量表达式替换。因此,以下内容仍适用于 OpenZeppelin Upgrades:

    //...  
    contract Example is BaseExample {    uint256 constant private_cap = 1000000000000000000;  
        //...}

限制 3:从合同代码创建新合约

根据合同代码创建合同的新实例时,这些创建将直接由 Solidity 处理,而不是由 OpenZeppelin Upgrades 处理,这意味着这些合同将不可升级。

例如,在以下示例中,即使 Example 是可升级的(如果通过 openzeppelin create Example 创建),创建的 Token 合约也不是可升级的:

    //...  
    contract Example is BaseExample {    uint256 private_cap = 1000000000000000000;    ERC20Capped public token;  
        function initialize(uint8 cap) initializer public {       _cap = cap;        token = new ERC20Capped(_cap);    }}

解决方法 1:从 CLI 注入预先部署的合同

解决此问题的最简单方法是避免完全自己创建合同:与其在 initialize 函数中创建合同,不如简单地接受该合同的实例作为参数,并在通过 OpenZeppelin CLI 创建合同后将其注入:

    //...  
    contract Example is BaseExample {    ERC20Capped public token;  
        function initialize(ERC20Capped_token) initializer public {        token =_token;    }}
    $ TOKEN=$(npx openzeppelin create TokenContract)$ npx oz create Example --init --args $TOKEN

解决方法2:OpenZeppelin App Contract

如果您需要即时创建可升级的合同,则一种高级替代方法是在合同中保留 OpenZeppelin 项目的 App 实例。`App 是一个合同,充当着 OpenZeppelin 项目的入口点,它引用了逻辑实现,并且可以创建新的合同实例:

    // contracts/Example.solpragma solidity ^0.6.0;  
    import "@openzeppelin/upgrades/contracts/Initializable.sol";import "@openzeppelin/upgrades/contracts/application/App.sol";  
    contract BaseExample is Initializable {    //...}  
    contract Example is BaseExample {  
      App private app;  
      function initialize(App_app) initializer public {    app =_app;  }  
      function createNewToken() public returns(address) {    return app.create("@openzeppelin/contracts-ethereum-package", "ERC20Capped");  }}

潜在的不安全操作

使用可升级的智能合约时,您将始终与代理合约实例进行交互,而不与基础逻辑(实现)合约进行交互。但是,没有什么可以阻止恶意参与者直接将交易发送到逻辑合约。这不会构成威胁,因为逻辑合同状态的任何更改都不会影响您的代理合同实例,因为逻辑合同的存储从未在您的项目中使用。

但是,有一个例外。如果对逻辑合约的直接调用触发了自毁操作 selfdestruct ,则逻辑合约将被销毁,并且所有合约实例将最终将所有调用委托给一个没有任何代码的地址。这将破坏项目中的所有合同实例。

如果逻辑合约包含委托调用 delegatecall 操作,则可以实现类似破坏效果。如果合同将调用委托到包含自毁的恶意合同,则调用合同也将被销毁。

    pragma solidity ^0.6.0;  
    // The Exmaple contract makes a `delegatecall` to the Malicious contract. Thus, even if the Malicious contract runs the `selfdestruct` function, it is run in the context of the Example contract, thus killing the Example contract.    
    contract Example {    function testFunc(address malicious) public {        malicious.delegatecall(abi.encodeWithSignature("kill()"));    }}  
    contract Malicious {    function kill() public {        address payable addr = address(uint160(address(0x4Bf8c809c898ee52Eb7fc6e1FdbB067423326B2A)));        selfdestruct(addr);    }}

因此,强烈建议您避免在合同中使用自毁 selfdestruct 或委托调用 delegatecall。如果需要包括它们,请绝对确保攻击者无法在未初始化的逻辑合约上调用它们。

修改合约

由于新功能或错误修复,在编写合同的新版本时,还要遵守其他限制:您不能更改合同状态变量的声明顺序或类型。您可以通过了解代理 [23] 来了解有关此限制背后原因的更多信息。

违反变量存储布局限制将导致合同的升级的版本混淆存储值,并可能导致程序严重错误。

这意味着如果您有如下所示初始合同:

    pragma solidity ^0.6.3;  
    contract Example {    string public tokenName;    uint8 public decimals;}

您就不能更改变量的类型:

    pragma solidity ^0.6.3;  
    contract Example {    string public tokenName;    uint public decimals;  // 类型变化了 , 不可以}

或更改声明它们的顺序:

    pragma solidity ^0.6.3;  
    contract Example {    uint public decimals;    string public tokenName;}

或者在现有变量之前引入一个新变量:

    pragma solidity ^0.6.3;  
    contract Example {    string public tokenSymbol;    string public tokenName;    uint public decimals;}

或删除现有变量:

    pragma solidity ^0.6.3;  
    contract Example {    string public tokenName;}

如果需要引入新变量,请确保始终在最后追加:

    pragma solidity ^0.6.3;  
    contract Example {    string public tokenName;    uint public decimals;    string public tokenSymbol;}

请记住,如果重命名变量,则升级后它将保持与以前相同的值。如果新变量在语义上与旧变量相同,则这也许是期望的行为 .

    pragma solidity ^0.6.3;  
    contract Example {    string public tokenName;    uint public decimalCount;    // starts with the value of `decimals`}

并且,如果您从合同末尾删除变量,请注意这实际上不会清除存储。随后添加新变量的更新将导致该变量从已删除的变量中读取剩余的值。

    pragma solidity ^0.6.3;  
    contract Example1 {    string public tokenName;    uint public decimals;}  
    // Updating Example1 --> Example2  
    contract Example2 {    string public tokenName;}  
    // Updating Example2 --> Example3  
    contract Example3 {    string public tokenName;    uint public decimalCount;    // starts with the value of `decimals`}

请注意,您可能还无意中通过更改合同的基类合同来更改合同的状态变量。例如,如果您有以下合同:

    pragma solidity ^0.6.3;  
    contract BaseExample1 {    uint256 createdAt;}  
    contract BaseExample2 {    string version;}  
    contract Example is BaseExample1, BaseExample2 {}

然后,通过改变声明基类合同的顺序,添加新的基类合同或删除基类合同来修改示例,将更改变量的实际存储布局:

    pragma solidity ^0.6.3;  
    contract BaseExample1 {    uint256 createdAt;}  
    contract BaseExample2 {    string version;}  
    //swapping the order in which the base contracts are declaredcontract Example is BaseExample2, BaseExample1 {}  
    //Or...  
    //removing base contract(s)contract Example is BaseExample1 {}  
    //Or...  
    contract BaseExample3 {}   
    //adding new base contractcontract Example is BaseExample1, BaseExample2, BaseExample3 {}

如果子合同有其自己的任何变量,也不能将新变量添加到基类合同。例如以下情况:

    pragma solidity ^0.6.3;  
    contract BaseExample {}  
    contract Example is BaseExample {    string tokenName;}  
    //Now, if the BaseExample is updated to the following  
    contract BaseExample {    string version;        // takes the value of `tokenName` }  
    contract Example is BaseExample {    string tokenName;}

以上,变量 version 会分配 tokenName 在先前版本中具有的插槽。

如果子合同有自己的变量,则还可能从基类合同中删除变量。例如:

    pragma solidity ^0.6.3;  
    contract BaseExample {    uint256 createdAt;    string version;}  
    contract Example is BaseExample {    string tokenName;}  
    //Now, if the BaseExample is updated to the following  
    contract BaseExample {    uint256 createdAt; }  
    contract Example is BaseExample {    string tokenName;    //takes the value of `version`}

在这里,当我们从 BaseExample 中删除了 version 变量时,tokenName (更新之后)现在将使用 version 的内存插槽(更新之前)。

一种解决方法是在将来可能要扩展的基类合同上声明预先未使用的变量,以作为“保留”这些插槽的一种方法。因此,基本上,对于所有更新,父合约和子合约中变量的数量和顺序都相同。

    pragma solidity ^0.6.3;  
    contract BaseExample {    string someVar1;    string someVar2;    string someVar3;  
        //...}

请注意,此技巧不涉及 gas 使用的增长问题 。

声明

我们从 OpenZeppelin[24]&NuCypher[25] 的这些出色的文档中复制了一些文本。

•NuCypher 的可升级合同方法 [26]•升级智能合约 [27]•撰写可升级合同 [28]

翻译自:https://simpleaswater.com/upgradable-smart-contracts/

本文首发于深入浅出区块链,这里是国内最受欢迎的区块链技术社区,你还没有加入 [29] 吗?

References

[1] OpenZeppelin CLI: https://www.npmjs.com/package/@openzeppelin/cli
[2] OpenZeppelin: https://www.npmjs.com/package/@openzeppelin/upgrades
[3] OpenZeppelin Upgrades: #OpenZeppelin-Upgrades
[4] 百万美元的以太币被盗 / 被黑客入侵 : https://medium.com/firmonetwork/3-famous-smart-contract-hacks-you-should-know-dffa6b934750
[5] 接口 : https://learnblockchain.cn/docs/solidity/contracts.html#interfaces
[6] 委托调用 deletage call : https://medium.com/coinmonks/delegatecall-calling-another-contract-function-in-solidity-b579f804178c
[7] fallback 回退函数 : https://www.tutorialspoint.com/solidity/solidity_fallback_function.htm
[8] fallback 函数 : https://www.tutorialspoint.com/solidity/solidity_fallback_function.htm
[9] 操作码 : https://medium.com/@blockchain101/solidity-bytecode-and-opcode-basics-672e9b1a88c2
[10] 这篇文章 : https://simpleaswater.com/how-upgradable-smart-contracts-work-under-the-hood/
[11] OpenZeppelin: https://openzeppelin.com/
[12] CLI 工具和库 : https://docs.openzeppelin.com/upgrades/2.7/
[13] 此处 : https://github.com/simpleaswater/upgradable-smart-contracts
[14] 一些文件 : https://docs.openzeppelin.com/cli/2.7/configuration#configuration-files-in-version-control
[15] Ganache: https://docs.openzeppelin.com/learn/deploying-and-interacting#local-blockchain
[16] OpenZeppelin Upgrades 升级库 : https://www.npmjs.com/package/@openzeppelin/upgrades
[17] 此处 : https://github.com/simpleaswater/upgradable-smart-contracts/blob/master/index.js
[18] 安装 NodeJs 和 Ganache: https://simpleaswater.com/upgradable-smart-contracts/#openZeppelin-upgrades-cli
[19] EVM 的工作原理 : https://www.bitrates.com/guides/ethereum/what-is-the-unstoppable-world-computer
[20] 此文章 : https://simpleaswater.com/how-upgradable-smart-contracts-work-under-the-hood/
[21] ERC20Capped: https://github.com/OpenZeppelin/openzeppelin-contracts-ethereum-package/blob/v2.0.2/contracts/token/ERC20/ERC20Capped.sol
[22] 不会为这些变量保留存储槽 : https://learnblockchain.cn/docs/solidity/contracts.html#constant
[23] 代理 : https://docs.openzeppelin.com/upgrades/2.7/proxies
[24] OpenZeppelin: https://docs.openzeppelin.com/openzeppelin/
[25] NuCypher: https://docs.nucypher.com/
[26] NuCypher 的可升级合同方法 : https://docs.nucypher.com/en/latest/architecture/upgradeable_proxy_contracts.html
[27] 升级智能合约 : https://docs.openzeppelin.com/learn/upgrading-smart-contracts
[28] 撰写可升级合同 : https://docs.openzeppelin.com/upgrades/2.7/writing-upgradeable
[29] 加入 : https://learnblockchain.cn/register?invite_code=48495FCD2

_
_

[译] 如何利用 OpenZeppelin 编写可升级的智能合约

__

来源链接:mp.weixin.qq.com