61

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

 5 years ago
source link: http://v1.8btc.com/smart-contracts-upgradable?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

EbABnye.jpg!web

主从合约(Master-Slave contract)

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

永久存储合约(Eternal Storage contract)

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

可升级存储代理合约

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

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

  1. 代理合约(Proxy contract) :它将充当永久存储并负责委托调用逻辑合约;
  2. 逻辑合约(Logic contract) :它负责完成处理所有的数据;
  3. 存储结构(Storage structure) :它包含了存储结构,并会由代理合约和逻辑合约所继承,以便它们的存储指针能够在区块链上保持同步;

iANzI3U.png!web

委托调用

该技术的核心在于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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK