如何实现可升级的智能合约?

知识
  • 区块网
  • 2018-09-18 17:56

智能合约的重要性已越来越明显,现如今,整个密码货币生态系统都是由智能合约所驱动!不管我们有多小心,或者我们的代码测试工作做得有多好,如果我们创建的是一个复杂的系统,那我们就有必要更新合约逻辑,以修补其存在的漏洞,或者添加必要的缺失功能。有时候,由于EVM虚拟机的更改或者被新发现的漏洞,我们可能需要去升级我们的智能合约。

一般来说,开发人员可以很容易地升级他们的软件,但区块链的情况是不一样滴,因为它们有着难以更改的属性。如果我们部署了一个合约,这就好比是泼出去的水。然而,如果我们使用适当的技术,我们可以在不同的地址部署一个新的合约,并使得旧合约无效。下面是一些最常见的,创建可升级智能合约的方法。

主从合约(Master-Slave contract)

主从技术,是可实现升级智能合约最为基础也是最容易理解的技术之一。在这种技术当中,我们部署一个主合约,以及其他合约,其中主合约负责存储所有其他合约的地址,并在需要时返回所需的地址。当这些合约需要和其它合约进行沟通时,它们会充当从合约,从主合约那里获取其它合约的最新地址。为了升级智能合约,我们只需要在网络上部署它,并更改主合约中的地址。虽然这远不是发展可升级智能合约的最佳方式,但它确是最简单的。这种方法存在着很多的局限性,其中之一是,我们不能轻易地把合约的数据或资产迁移到新合约中。

永久存储合约(Eternal Storage contract)

在这种技术当中,我们将逻辑合约和数据合约彼此分离。数据合约应该是永久并且不可升级的。而逻辑合约可以根据需要进行多次升级,并将变化通知给数据合约。这是一项相当基本的技术,并且存在着一个明显的缺陷。由于数据合约是不可升级的,数据结构中需要的任何更改,或数据合约中存在的漏洞,都会导致所有数据变得无用。这种技术的另一个问题是,如果逻辑合约想要访问/操作区块链上的数据,那么这个逻辑合约将需要进行外部调用,而外部调用会消耗额外的gas。通常情况下,这种技术会和主从技术相结合,以促进合约间的通信。

可升级存储代理合约

我们可通过使永久存储合约充当逻辑合约的代理,以此防止支付额外的gas。这个代理合约,以及这个逻辑合约,将继承同一存储合约,那么它们的存储会在EVM虚拟机中对齐。这个代理合约将有一个回退函数,它将委托调用这个逻辑合约,那么这个逻辑合约就可以在代理存储中进行更改。这个代理合约将是永恒的。这节省了对存储合约多次调用所需的gas,不管数据做了多少的更改,就只需要一次委托调用。

这项技术当中有三个组成部分:

代理合约(Proxy contract)

它将充当永久存储并负责委托调用逻辑合约;

逻辑合约(Logic contract)

它负责完成处理所有的数据;

存储结构(Storage structure)

它包含了存储结构,并会由代理合约和逻辑合约所继承,以便它们的存储指针能够在区块链上保持同步;

委托调用

该技术的核心在于EVM所提供的DELEGATECALL操作码,DELEGATECALL就像是一个普通的CALL 调用操作码,不同之处在于目标地址上的代码是在调用合约上下文中执行的,而原始调用的msg.sender以及msg.value将被保留。简单说,DELEGATECALL基本上允许(委托)目标合约在调用合约的存储中做它任何想做的事情。我们将利用这一点,并创建一个代理合约,它将使用DELEGATECALL操作码委托调用逻辑合约,这样我们就可以在代理合约中保持数据的安全,同时我们可以自由地更改逻辑合约。如何使用可升级存储代理合约?让我们深入研究一下细节。我们需要的第一个合约是存储结构。它将定义我们需要的所有存储变量,并将由代理合约和执行合约所继承。它看起来会是这样的:

contract StorageStructure {

address public implementation;address public owner;mapping (address => uint) internal points;uint internal totalPlayers;}

我们现在需要一个执行/逻辑合约。让我们创建一个简单版的合约,在添加新玩家时不会增加totalPlayers计数器的数字。

contract ImplementationV1 is StorageStructure {

modifier onlyOwner() {require (msg.sender == owner);_;}function addPlayer(address _player, uint _points)public onlyOwner{require (points[_player] == 0);points[_player] = _points;}function setPoints(address _player, uint _points)public onlyOwner{require (points[_player] != 0);points[_player] = _points;}}

下面就是最关键的部分:代理合约;

contract Proxy is StorageStructure {

modifier onlyOwner() {require (msg.sender == owner);_;}/*** @dev constructor that sets the owner address*/constructor() public {owner = msg.sender;}/*** @dev Upgrades the implementation address* @param _newImplementation address of the new implementation*/function upgradeTo(address _newImplementation)external onlyOwner{require(implementation != _newImplementation);_setImplementation(_newImplementation);}/*** @dev Fallback function allowing to perform a delegatecall* to the given implementation. This function will return* whatever the implementation call returns*/function () payable public {address impl = implementation;require(impl != address(0));assembly {let ptr := mload(0x40)calldatacopy(ptr, 0, calldatasize)let result := delegatecall(gas, impl, ptr, calldatasize, 0, 0)let size := returndatasizereturndatacopy(ptr, 0, size)switch resultcase 0 { revert(ptr, size) }default { return(ptr, size) }}}/*** @dev Sets the address of the current implementation* @param _newImp address of the new implementation*/function _setImplementation(address _newImp) internal {implementation = _newImp;}}

为了让合约生效,我们首先需要部署代理合约以及ImplementationV1合约,然后调用这个代理合约的upgradeTo(address)函数,同时pass掉我们的ImplementationV1合约地址。现在,我们可以忘记这个ImplementationV1合约的地址,并把代理合约的地址作为我们的主地址。

为了升级这个合约,我们需要创建一个新的逻辑合约实现,它可以是这样的:

contract ImplementationV2 is ImplementationV1 {

function addPlayer(address _player, uint _points)public onlyOwner{require (points[_player] == 0);points[_player] = _points;totalPlayers++;}}

你应该注意到,这个合约也继承了存储结构合约(StorageStructure contract),尽管它是间接地。

所有的执行方案都必须继承这个存储结构合约,并且在部署代理合约后不得进行更改,以避免对代理的存储进行意外覆盖。为了实现升级,我们在网络上部署这个合约,然后调用代理合约的upgradeTo(address) 函数,同时pass掉ImplementationV2合约的地址。这种技术,使得升级合约逻辑变得相当容易,但它仍然不允许我们升级合约的存储结构。我们可以通过使用非结构化的代理合约来解决这个问题。

非结构化可升级存储代理合约

这是当前最先进的,可实现智能合约升级的方法之一。它通过保存合约地址以及在存储中固定位置所有者的方法,以实现它们不会被执行/逻辑合约提供的数据所覆盖。我们可以使用sload以及sstore操作码来直接读取和写入由固定指针所引用的特定存储槽。此方法利用了存储中状态变量的布局,以避免逻辑合约覆盖掉固定位置。如果我们将固定位置设置为0x7,那么在使用前7个存储槽后,它就会被覆盖掉。为了避免这种情况,我们将固定位置设置为类似keccak256(“org.govblocks.implemenation.address”).这消除了在代理合约中继承存储结构合约的需要,这意味着我们现在也可以升级存储结构了。然而,升级存储结构是一项棘手的任务,因为我们需要确保,我们所提交的更改,不会导致新的存储布局与先前的存储布局不匹配。这项技术有两个组成部分。1、代理合约:它负责将执行合约的地址存储在一个固定的地址当中,并负责委托调用它; 2、执行合约:它是主要合约,负责把我逻辑以及存储结构;你甚至可以将这项技术用于你现有的合约,因为它不需要对你的执行合约进行任何更改。

这个代理合约会是这样子的:

contract UnstructuredProxy {

// Storage position of the address of the current implementationbytes32 private constant implementationPosition =keccak256("org.govblocks.implementation.address");// Storage position of the owner of the contractbytes32 private constant proxyOwnerPosition =keccak256("org.govblocks.proxy.owner");/*** @dev Throws if called by any account other than the owner.*/modifier onlyProxyOwner() {require (msg.sender == proxyOwner());_;}/*** @dev the constructor sets owner*/constructor() public {_setUpgradeabilityOwner(msg.sender);}/*** @dev Allows the current owner to transfer ownership* @param _newOwner The address to transfer ownership to*/function transferProxyOwnership(address _newOwner)public onlyProxyOwner{require(_newOwner != address(0));_setUpgradeabilityOwner(_newOwner);}/*** @dev Allows the proxy owner to upgrade the implementation* @param _implementation address of the new implementation*/function upgradeTo(address _implementation)public onlyProxyOwner{_upgradeTo(_implementation);}/*** @dev Tells the address of the current implementation* @return address of the current implementation*/function implementation() public view returns (address impl) {bytes32 position = implementationPosition;assembly {impl := sload(position)}}/*** @dev Tells the address of the owner* @return the address of the owner*/function proxyOwner() public view returns (address owner) {bytes32 position = proxyOwnerPosition;assembly {owner := sload(position)}}/*** @dev Sets the address of the current implementation* @param _newImplementation address of the new implementation*/function _setImplementation(address _newImplementation)internal{bytes32 position = implementationPosition;assembly {sstore(position, _newImplementation)}}/*** @dev Upgrades the implementation address* @param _newImplementation address of the new implementation*/function _upgradeTo(address _newImplementation) internal {address currentImplementation = implementation();require(currentImplementation != _newImplementation);_setImplementation(_newImplementation);}/*** @dev Sets the address of the owner*/function _setUpgradeabilityOwner(address _newProxyOwner)internal{bytes32 position = proxyOwnerPosition;assembly {sstore(position, _newProxyOwner)}}}

如何使用非结构化可升级存储代理合约?

使用非结构化可升级存储代理合约是非常简单的,因为这种技术几乎可以处理所有现有的合约。想要使用这种技术,你只需要遵循以下步骤:1.部署代理合约和执行合约;2.调用代理合约的upgradeTo(address)函数,同时pass掉执行合约的地址。我们现在可以忘掉这个执行合约地址,然后把代理合约的地址作为主地址。而要升级这个新实施的合约,我们只需要部署新的执行合约,并调用代理合约的upgradeTo(address) 函数,同时pass掉这个新执行合约的地址。就是这么简单!让我们简单举个例子。我们将再次使用上述可升级存储代理合约中使用的同一逻辑合约,但是我们不需要用到存储结构。因此,我们的ImplementationV1合约看起来会是这样的:

contract ImplementationV1 {

address public owner;mapping (address => uint) internal points;modifier onlyOwner() {require (msg.sender == owner);_;}function initOwner() external {require (owner == address(0));owner = msg.sender;}function addPlayer(address _player, uint _points)public onlyOwner{require (points[_player] == 0);points[_player] = _points;}function setPoints(address _player, uint _points)public onlyOwner{require (points[_player] != 0);points[_player] = _points;}}

下一步是部署这个执行合约以及我们的代理合约。然后,再调用代理合约的upgradeTo(address) 函数,同时pass掉执行合约的地址。

你可能注意到,在这个执行合约中,甚至没有声明totalPlayers变量,我们可以升级这个执行合约,其中具有 totalPlayers变量,这个新的执行合约看起来会是这样的:

contract ImplementationV2 is ImplementationV1 {

uint public totalPlayers;function addPlayer(address _player, uint _points)public onlyOwner{require (points[_player] == 0);points[_player] = _points;totalPlayers++;}}

而要升级这个新的执行合约,我们需要做的,就是在网络上部署这个合约,然后,嗯你猜对了,就是调用代理合约的upgradeTo(address)函数,并同时pass掉我们新执行合约的地址。现在,我们的合约已演变为能够保持跟踪 totalPlayers,同时仍然为用户提供相同的地址。

这种方法是强大的,但也存在着一些局限性。主要关注的一点是,代理合约拥有者(proxyOwner)有太多的权力。而且,这种方法对复杂的系统而言是不够的。对于构建具有可升级合约的 dApp而言,组合主从合约以及非结构化可升级存储代理合约,会是更为灵活的一种方法,这也是作者所在的GovBlocks所使用的方法。结论非结构化存储代理合约,是创建可升级智能合约最先进的技术之一,但它仍然是不完美的。毕竟,我们并不希望dApp所有者对dApp具有不当的控制权。如果开发者拥有了这种权力,那这个dapp还能称之为去中心化应用吗?


来源:区块网作者:摘编编辑:gageerun

本文链接: https://www.dyqklw.com/article/20180918/531.html

声明:除非注明,本站文章均为第一区块链网原创或编译,转载时请注明文章作者和“来源:第一区块链网”,本站尊重行业规范,每篇文章都标有明确的作者和来源。文章为作者观点,不代表第一区块链网立场。

免责:阁下应知本网站的任何内容仅供参考,不能做为投资决策依据,投资有风险,入市须警慎!请谨防ICO、变相ICO!

相关文章

资讯

原创

荐读

热门标签