前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【翻译】编写可升级的智能合约

【翻译】编写可升级的智能合约

作者头像
Tiny熊
发布2021-01-14 16:16:00
2.5K0
发布2021-01-14 16:16:00
举报
文章被收录于专栏:深入浅出区块链技术

原文链接

当使用OpenZeppelin Upgrades编写可升级合约时,有一些在编写Solidity代码时需要记住一些注意事项。

值得一提的是,这些限制源于以太坊虚拟机的工作方式,并且适用于所有使用可升级合约的项目,而不仅仅是OpenZeppelin Upgrades。

初始化器(Initializers)

在编写Solidity合约使用OpenZeppelin Upgrades,无需任何修改,只需要修改构造函数。由于基于代理的可升级性系统的要求,可升级合约中不能使用构造函数。要了解这个限制背后的原因,请查看代理。

这意味着,当使用OpenZeppelin可升级的合约时,您需要将其构造函数改为一个常规函数,通常命名为initialize,在那里执行所有的初始化逻辑。

代码语言:javascript
复制
// NOTE: Do not use this code snippet, it's incomplete and has a critical vulnerability!

pragma solidity ^0.6.0;


contract MyContract {
    uint256 public x;

    function initialize(uint256 _x) public {
        x = _x;
    }
}

然而,虽然Solidity确保一个构造函数constructor在合约的生命周期内只被调用一次,但一个普通函数可以被多次调用。为了防止一个合约被多次初始化,你需要添加一个检查来确保初始化函数只被调用一次。

代码语言:javascript
复制
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;


contract MyContract {
    uint256 public x;
    bool private initialized;

    function initialize(uint256 _x) public {
        require(!initialized, "Contract instance has already been initialized");
        initialized = true;
        x = _x;
    }
}

由于这种模式在编写可升级合约时非常常见,OpenZeppelin Upgrades提供了一个Initializable基础合约,它有一个initializer modifier来处理这个问题。

代码语言:javascript
复制
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract MyContract is Initializable {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        x = _x;
    }
}

构造函数constructor和普通函数的另一个区别是,Solidity负责自动调用一个合约的所有基类的构造函数。在编写初始化器initializer时,你需要特别注意手动调用所有父合约的初始化器initializer。

代码语言:javascript
复制
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseContract is Initializable {
    uint256 public y;

    function initialize() public initializer {
        y = 42;
    }
}


contract MyContract is BaseContract {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        BaseContract.initialize(); // Do not forget this call!
        x = _x;
    }
}

使用可升级的智能合约库

请记住,这个限制不仅会影响你的合约,还会影响你从库中导入的合约。例如考虑OpenZeppelin合约中的ERC20:该合约在其构造函数中初始化了token的名称、符号和小数位数。

代码语言:javascript
复制
// @openzeppelin/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.6.0;

  ...

contract ERC20 is Context, IERC20 {

  ...

    string private _name;
    string private _symbol;
    uint8 private _decimals;

    constructor (string memory name, string memory symbol) public {
        _name = name;
        _symbol = symbol;
        _decimals = 18;
    }

  ...
}

这意味着你不应该在你的OpenZeppelin Upgrades项目中使用这些合约。相反,请确保使用@openzeppelin/contracts-upgradeable,它是OpenZeppelin合约的官方分支,已经被修改为使用初始化器而不是构造函数。看看在@openzeppelin/contracts-upgradeable中的ERC20Upgradeable是什么样子:

代码语言:javascript
复制
// @openzeppelin/contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol
pragma solidity ^0.6.0;
  ...
contract ERC20Upgradeable is Initializable, ContextUpgradeable, IERC20Upgradeable {
  ...
    string private _name;
    string private _symbol;
    uint8 private _decimals;

    function __ERC20_init(string memory name, string memory symbol) internal initializer {
        __Context_init_unchained();
        __ERC20_init_unchained(name, symbol);
    }

    function __ERC20_init_unchained(string memory name, string memory symbol) internal initializer {
        _name = name;
        _symbol = symbol;
        _decimals = 18;
    }
  ...
}

无论是使用OpenZeppelin合约还是其他智能合约库,都要确保软件包被设置为处理可升级合约。

在合约中了解更多关于OpenZeppelin合约可升级的信息:Contracts: Using with Upgrades。

避免在字段声明中使用初始值

Solidity允许在合约中声明字段时为其定义初始值。

代码语言:javascript
复制
contract MyContract {
    uint256 public hasInitialValue = 42; // equivalent to setting in the constructor
}

这相当于在构造函数中设置了这些值,因此,对于可升级的合约是无效的。请确保所有初始值都在如下所示初始化函数中设置;否则,任何可升级的实例都不会设置这些字段。

代码语言:javascript
复制
contract MyContract is Initializable {
    uint256 public hasInitialValue;

    function initialize() public initializer {
        hasInitialValue = 42; // set initial value in initializer
    }
}

注意 定义常量状态变量还是可以的,因为编译器并没有为这些变量预留存储槽,每出现一次就会被相应的常量表达式所替代。所以下面的内容在OpenZeppelin Upgrades中仍然可以使用:

代码语言:javascript
复制
contract MyContract {
    uint256 public constant hasInitialValue = 42; // define as constant
}

从合约代码中创建新实例

当从合约代码中创建一个新的合约实例时,这些创建直接由Solidity处理,而不是由OpenZeppelin Upgrades处理,这意味着这些合约将无法升级。

例如,在下面的例子中,即使MyContract被部署为可升级,创建的token合约也是不可升级的:

代码语言:javascript
复制
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyContract is Initializable {
    ERC20 public token;

    function initialize() public initializer {
        token = new ERC20("Test", "TST"); // This contract will not be upgradeable
    }
}

如果你希望ERC20实例可以升级,最简单的实现方式就是直接地接受该合约的实例作为参数,并在创建后接管它:

代码语言:javascript
复制
// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts-upgradeable/contracts/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol";

contract MyContract is Initializable {
    IERC20 public token;

    function initialize(IERC20Upgradeable _token) public initializer {
        token = _token;
    }
}

潜在的不安全操作

在使用可升级的智能合约时,你将始终与(代理)合约实例进行交互,而不是底层逻辑合约。然而我们却无法阻止恶意行为者直接向逻辑合约发送交易。这不会构成威胁,因为逻辑合约状态的任何变化都不会影响你的(代理)合约实例,因为你的项目中从未使用过逻辑合约的存储。

然而,有一个例外。如果对逻辑合约的直接调用触发了自毁操作selfdestruct,那么逻辑合约就会被销毁,你的所有合约实例最终都会将所有的调用委托给一个地址,而不会有任何代码。这会破坏你项目中的所有合约实例。

如果逻辑合约中包含委托调用delegatecall操作,也可以达到类似的效果。如果可以将delegatecall变成一个包含自毁的恶意合约,那么调用合约将被破坏。

因此,在你的合约中不允许使用selfdestructdelegatecall

修改你的合约

在编写新版本的合约时,无论是由于新功能还是bug修复,都有一个额外的限制需要遵守:你不能改变合约状态变量的声明顺序,也不能改变它们的类型。你可以通过了解 Proxies来阅读更多关于这个限制背后的原因。

警告 违反这些存储布局限制中的任何一项,都会导致升级版的合约的存储值被混淆,并可能导致你的应用程序出现关键错误。这意味着,如果初始合约看起来像这样:

代码语言:javascript
复制
contract MyContract {
    uint256 private x;
    string private y;
}

那么不可以修改合约变量类型:

代码语言:javascript
复制
contract MyContract {
    string private x;
    string private y;
}

也无法改变变量的声明顺序:

代码语言:javascript
复制
contract MyContract {
    string private y;
    uint256 private x;
}

不能在现有变量之前引入新的变量:

代码语言:javascript
复制
contract MyContract {
    bytes private a;
    uint256 private x;
    string private y;
}

也不能删除现有变量:

代码语言:javascript
复制
contract MyContract {
    string private y;
}

如果需要引入新的变量,请确保添加到原有变量的后面:

代码语言:javascript
复制
contract MyContract {
    uint256 private x;
    string private y;
    bytes private z;
}

注意,如果重命名一个变量,那么在升级后,它将保持与之前相同的值。如果新变量和旧变量的语义相同,那么这可能是我们所希望的行为:

代码语言:javascript
复制
contract MyContract {
    uint256 private x;
    string private z; // starts with the value from `y`
}

而如果你在合约的最后删除了一个变量,请注意存储不会被清除。随后的更新中如果增加一个新的变量,会导致该变量从被删除的变量中读取遗留的值:

代码语言:javascript
复制
contract MyContract {
    uint256 private x;
}

升级到:

代码语言:javascript
复制
contract MyContract {
    uint256 private x;
    string private z; // starts with the value from `y`
}

注意,你也可能会因为改变合约的父合约而无意中改变合约的存储变量。例如,如果你有以下合约:

代码语言:javascript
复制
contract A {
    uint256 a;
}


contract B {
    uint256 b;
}


contract MyContract is A, B {}

然后通过调换基础合约的声明顺序或引入新的基础合约来修改MyContract,将改变变量的实际存储方式:

代码语言:javascript
复制
contract MyContract is B, A {}

如果集成合约有任何自己的变量,你也不能在基础合约中添加新的变量。鉴于以下情况:

代码语言:javascript
复制
contract Base {
    uint256 base1;
}


contract Child is Base {
    uint256 child;
}

如果修改Base,增加一个额外的变量:

代码语言:javascript
复制
contract Base {
    uint256 base1;
    uint256 base2;
}

然后,变量base2将被分配到上一个版本中那个child的槽位。一个变通的办法是在基础合约上声明未使用的变量,你可能会在未来想要扩展,作为 "保留 "这些槽位的一种手段。请注意,这个技巧不会增加gas使用量。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 初始化器(Initializers)
    • 使用可升级的智能合约库
      • 避免在字段声明中使用初始值
      • 从合约代码中创建新实例
      • 潜在的不安全操作
      • 修改你的合约
      相关产品与服务
      区块链
      云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档