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

  • Tiny熊
  • 更新于 2023-05-24 16:01
  • 阅读 10519

看看如何利用OpenZeppelin Upgrades 为我们的合约插上可升级的翅膀.

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

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

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

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

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

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

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

升级是如何进行的

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

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

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

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

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

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

img

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

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

img

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

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

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

OpenZeppelin Upgrades

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

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

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

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管理:您无需手动进行任何编辑。 但是,应该将其中一些文件提交给Git。

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

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

$ npx openzeppelin accounts

解锁了的账号

编写及部署合约

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

// contracts/TodoList.sol
pragma 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

deploy-contract

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

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

img

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

// contracts/TodoList.sol
pragma 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命令来升级之前部署的实例。

升级

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

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

img

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

 以编程方式升级合约

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

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

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

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

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

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

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

// index.js
const 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.sol
pragma 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.sol
pragma 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

运行

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

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

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

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

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

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

值得一提的是,这些限制源于EVM的工作原理,并且不仅适用于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:没有构造函数

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

解决方法:初始化

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

// 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;

    function initialize(uint8 cap) public {
        _cap = cap;
        token = new ERC20Capped(_cap);
    }
}

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

// 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;

    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.sol
pragma 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.sol
pragma 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-packageERC20Capped长什么样:

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.sol
pragma 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);
    }
}

请注意,设置常量还是可以的,因为编译器不会为这些变量保留存储槽,并且每次出现都将被相应的常量表达式替换。 因此,以下内容仍适用于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.sol
pragma 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。 如果需要包括它们,请绝对确保攻击者无法在未初始化的逻辑合约上调用它们。

修改合约

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

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

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

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 declared
contract Example is BaseExample2, BaseExample1 {}

//Or...

//removing base contract(s)
contract Example is BaseExample1 {}

//Or...

contract BaseExample3 {} 

//adding new base contract
contract 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 使用的增长问题 。

声明

我们从OpenZeppelinNuCypher的这些出色的文档中复制了一些文本。

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

点赞 10
收藏 16
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。