内容简介:智能合约的重要性已越来越明显,现如今,整个密码货币生态系统都是由智能合约所驱动!不管我们有多小心,或者我们的代码测试工作做得有多好,如果我们创建的是一个复杂的系统,那我们就有必要更新合约逻辑,以修补其存在的漏洞,或者添加必要的缺失功能。有时候,由于EVM虚拟机的更改或者被新发现的漏洞,我们可能需要去升级我们的智能合约。一般来说,开发人员可以很容易地升级他们的软件,但区块链的情况是不一样滴,因为它们有着难以更改的属性。如果我们部署了一个合约,这就好比是泼出去的水。然而,如果我们使用适当的技术,我们可以在不同
智能合约的重要性已越来越明显,现如今,整个密码货币生态系统都是由智能合约所驱动!不管我们有多小心,或者我们的代码测试工作做得有多好,如果我们创建的是一个复杂的系统,那我们就有必要更新合约逻辑,以修补其存在的漏洞,或者添加必要的缺失功能。有时候,由于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)
我们现在可以忘掉这个执行合约地址,然后把代理合约的地址作为主地址。
而要升级这个新实施的合约,我们只需要部署新的执行合约,并调用代理合约的 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还能称之为去中心化应用吗?
欢迎你给出自己的看法。
发文时比特币价格 ¥42871.86
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- MakerDAO治理合约升级背后的安全风波
- 智能合约攻击分析之庞氏代币合约漏洞
- 检测了3万多份智能合约,这份白皮书找到了9大智能合约安全漏洞(附下载链接)
- 智能合约工程
- 智能合约微服务
- 智能合约入门-new
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
HTML 编码/解码
HTML 编码/解码
RGB HSV 转换
RGB HSV 互转工具