如何实现可升级的智能合约?区块链
一般来说,开发人员可以很容易地升级他们的软件,但区块链的情况是不一样滴,因为它们有着难以更改的属性。
智能合约的重要性已越来越明显,现如今,整个密码货币生态系统都是由智能合约所驱动!不管我们有多小心,或者我们的代码测试工作做得有多好,如果我们创建的是一个复杂的系统,那我们就有必要更新合约逻辑,以修补其存在的漏洞,或者添加必要的缺失功能。有时候,由于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 := returndatasize returndatacopy(ptr, 0, size)switch result case 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 implementation bytes32 private constant implementationPosition = keccak256("org.govblocks.implementation.address");// Storage position of the owner of the contract bytes32 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) } } }
如何使用非结构化可升级存储代理合约?
使用非结构化可升级存储代理合约是非常简单的,因为这种技术几乎可以处理所有现有的合约。想要使用这种技术,你只需要遵循以下步骤:
部署代理合约和执行合约;
调用代理合约的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还能称之为去中心化应用吗?在这里,我建议读者可以阅读下Nitika提出的反对使用onlyOwner的论点。你也可以在GitHub上窥探到我们的代理合约。
希望这篇文章可以帮助你创建可升级的智能合约。
同时向Zepplin在代理技术方面进行的工作致敬。
1.TMT观察网遵循行业规范,任何转载的稿件都会明确标注作者和来源;
2.TMT观察网的原创文章,请转载时务必注明文章作者和"来源:TMT观察网",不尊重原创的行为TMT观察网或将追究责任;
3.作者投稿可能会经TMT观察网编辑修改或补充。